We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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`


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gitlab/base.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 
18 import importlib
19 
20 
21 class RESTObject(object):
22     """Represents an object built from server data.
23 
24     It holds the attributes know from the server, and the updated attributes in
25     another. This allows smart updates, if the object allows it.
26 
27     You can redefine ``_id_attr`` in child classes to specify which attribute
28     must be used as uniq ID. ``None`` means that the object can be updated
29     without ID in the url.
30     """
31 
32     _id_attr = "id"
33 
34     def __init__(self, manager, attrs):
35         self.__dict__.update(
36             {
37                 "manager": manager,
38                 "_attrs": attrs,
39                 "_updated_attrs": {},
40                 "_module": importlib.import_module(self.__module__),
41             }
42         )
43         self.__dict__["_parent_attrs"] = self.manager.parent_attrs
44         self._create_managers()
45 
46     def __getstate__(self):
47         state = self.__dict__.copy()
48         module = state.pop("_module")
49         state["_module_name"] = module.__name__
50         return state
51 
52     def __setstate__(self, state):
53         module_name = state.pop("_module_name")
54         self.__dict__.update(state)
55         self.__dict__["_module"] = importlib.import_module(module_name)
56 
57     def __getattr__(self, name):
58         try:
59             return self.__dict__["_updated_attrs"][name]
60         except KeyError:
61             try:
62                 value = self.__dict__["_attrs"][name]
63 
64                 # If the value is a list, we copy it in the _updated_attrs dict
65                 # because we are not able to detect changes made on the object
66                 # (append, insert, pop, ...). Without forcing the attr
67                 # creation __setattr__ is never called, the list never ends up
68                 # in the _updated_attrs dict, and the update() and save()
69                 # method never push the new data to the server.
70                 # See https://github.com/python-gitlab/python-gitlab/issues/306
71                 #
72                 # note: _parent_attrs will only store simple values (int) so we
73                 # don't make this check in the next except block.
74                 if isinstance(value, list):
75                     self.__dict__["_updated_attrs"][name] = value[:]
76                     return self.__dict__["_updated_attrs"][name]
77 
78                 return value
79 
80             except KeyError:
81                 try:
82                     return self.__dict__["_parent_attrs"][name]
83                 except KeyError:
84                     raise AttributeError(name)
85 
86     def __setattr__(self, name, value):
87         self.__dict__["_updated_attrs"][name] = value
88 
89     def __str__(self):
90         data = self._attrs.copy()
91         data.update(self._updated_attrs)
92         return "%s => %s" % (type(self), data)
93 
94     def __repr__(self):
95         if self._id_attr:
96             return "<%s %s:%s>" % (
97                 self.__class__.__name__,
98                 self._id_attr,
99                 self.get_id(),
100             )
101         else:
102             return "<%s>" % self.__class__.__name__
103 
104     def __eq__(self, other):
105         if self.get_id() and other.get_id():
106             return self.get_id() == other.get_id()
107         return super(RESTObject, self) == other
108 
109     def __ne__(self, other):
110         if self.get_id() and other.get_id():
111             return self.get_id() != other.get_id()
112         return super(RESTObject, self) != other
113 
114     def __dir__(self):
115         return super(RESTObject, self).__dir__() + list(self.attributes)
116 
117     def __hash__(self):
118         if not self.get_id():
119             return super(RESTObject, self).__hash__()
120         return hash(self.get_id())
121 
122     def _create_managers(self):
123         managers = getattr(self, "_managers", None)
124         if managers is None:
125             return
126 
127         for attr, cls_name in self._managers:
128             cls = getattr(self._module, cls_name)
129             manager = cls(self.manager.gitlab, parent=self)
130             self.__dict__[attr] = manager
131 
132     def _update_attrs(self, new_attrs):
133         self.__dict__["_updated_attrs"] = {}
134         self.__dict__["_attrs"].update(new_attrs)
135 
136     def get_id(self):
137         """Returns the id of the resource."""
138         if self._id_attr is None or not hasattr(self, self._id_attr):
139             return None
140         return getattr(self, self._id_attr)
141 
142     @property
143     def attributes(self):
144         d = self.__dict__["_updated_attrs"].copy()
145         d.update(self.__dict__["_attrs"])
146         d.update(self.__dict__["_parent_attrs"])
147         return d
148 
149 
150 class RESTObjectList(object):
151     """Generator object representing a list of RESTObject's.
152 
153     This generator uses the Gitlab pagination system to fetch new data when
154     required.
155 
156     Note: you should not instanciate such objects, they are returned by calls
157     to RESTManager.list()
158 
159     Args:
160         manager: Manager to attach to the created objects
161         obj_cls: Type of objects to create from the json data
162         _list: A GitlabList object
163     """
164 
165     def __init__(self, manager, obj_cls, _list):
166         """Creates an objects list from a GitlabList.
167 
168         You should not create objects of this type, but use managers list()
169         methods instead.
170 
171         Args:
172             manager: the RESTManager to attach to the objects
173             obj_cls: the class of the created objects
174             _list: the GitlabList holding the data
175         """
176         self.manager = manager
177         self._obj_cls = obj_cls
178         self._list = _list
179 
180     def __iter__(self):
181         return self
182 
183     def __len__(self):
184         return len(self._list)
185 
186     def __next__(self):
187         return self.next()
188 
189     def next(self):
190         data = self._list.next()
191         return self._obj_cls(self.manager, data)
192 
193     @property
194     def current_page(self):
195         """The current page number."""
196         return self._list.current_page
197 
198     @property
199     def prev_page(self):
200         """The previous page number.
201 
202         If None, the current page is the first.
203         """
204         return self._list.prev_page
205 
206     @property
207     def next_page(self):
208         """The next page number.
209 
210         If None, the current page is the last.
211         """
212         return self._list.next_page
213 
214     @property
215     def per_page(self):
216         """The number of items per page."""
217         return self._list.per_page
218 
219     @property
220     def total_pages(self):
221         """The total number of pages."""
222         return self._list.total_pages
223 
224     @property
225     def total(self):
226         """The total number of items."""
227         return self._list.total
228 
229 
230 class RESTManager(object):
231     """Base class for CRUD operations on objects.
232 
233     Derived class must define ``_path`` and ``_obj_cls``.
234 
235     ``_path``: Base URL path on which requests will be sent (e.g. '/projects')
236     ``_obj_cls``: The class of objects that will be created
237     """
238 
239     _path = None
240     _obj_cls = None
241 
242     def __init__(self, gl, parent=None):
243         """REST manager constructor.
244 
245         Args:
246             gl (Gitlab): :class:`~gitlab.Gitlab` connection to use to make
247                          requests.
248             parent: REST object to which the manager is attached.
249         """
250         self.gitlab = gl
251         self._parent = parent  # for nested managers
252         self._computed_path = self._compute_path()
253 
254     @property
255     def parent_attrs(self):
256         return self._parent_attrs
257 
258     def _compute_path(self, path=None):
259         self._parent_attrs = {}
260         if path is None:
261             path = self._path
262         if self._parent is None or not hasattr(self, "_from_parent_attrs"):
263             return path
264 
265         data = {
266             self_attr: getattr(self._parent, parent_attr, None)
267             for self_attr, parent_attr in self._from_parent_attrs.items()
268         }
269         self._parent_attrs = data
270         return path % data
271 
272     @property
273     def path(self):
274         return self._computed_path
275
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/gitlab/base.py b/gitlab/base.py
--- a/gitlab/base.py
+++ b/gitlab/base.py
@@ -131,7 +131,7 @@
 
     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."""

{"golden_diff": "diff --git a/gitlab/base.py b/gitlab/base.py\n--- a/gitlab/base.py\n+++ b/gitlab/base.py\n@@ -131,7 +131,7 @@\n \n     def _update_attrs(self, new_attrs):\n         self.__dict__[\"_updated_attrs\"] = {}\n-        self.__dict__[\"_attrs\"].update(new_attrs)\n+        self.__dict__[\"_attrs\"] = new_attrs\n \n     def get_id(self):\n         \"\"\"Returns the id of the resource.\"\"\"\n", "issue": "RefreshMixin.refresh() doesn't remove removed attributes\n## Description of the problem, including code/CLI snippet\r\n\r\nWhen attributes disappear from an object on the server `RefreshMixin.refresh()` doesn't remove them.\r\n\r\nFor 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.\r\n\r\n```bash\r\n# get a job with artifacts\r\njob = project.jobs.get(job_id)\r\n# will succeed\r\nassert hasattr(job, \"artifacts_file\")\r\n# now delete the artifacts from the server\r\njob.delete_artifacts()\r\n\r\n# This will fail because the artifacts_file is still there; refresh() didn't remove it\r\njob.refresh()\r\nassert not hasattr(job, \"artifacts_file\")\r\n\r\n# If you get the job again from the project it'll be fine\r\njob = project.jobs.get(job_id)\r\nassert not hasattr(job, \"artifacts_file\")\r\n```\r\n\r\n```python\r\n\r\n```\r\n\r\n\r\n## Expected Behavior\r\n\r\nI 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()`\r\n\r\n```python\r\no.refresh()\r\n# After a refresh this should always be true\r\no.attributes == o.manager.get(o.id).attributes\r\n```\r\n\r\n## Actual Behavior\r\n\r\nThey're not equal\r\n\r\n## Specifications\r\n\r\n  - python-gitlab version: `v2.4.0`\r\n  - API version you are using (v3/v4): `v4`\r\n  - Gitlab server version (or gitlab.com): `13.2.3`\r\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Lesser General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport importlib\n\n\nclass RESTObject(object):\n    \"\"\"Represents an object built from server data.\n\n    It holds the attributes know from the server, and the updated attributes in\n    another. This allows smart updates, if the object allows it.\n\n    You can redefine ``_id_attr`` in child classes to specify which attribute\n    must be used as uniq ID. ``None`` means that the object can be updated\n    without ID in the url.\n    \"\"\"\n\n    _id_attr = \"id\"\n\n    def __init__(self, manager, attrs):\n        self.__dict__.update(\n            {\n                \"manager\": manager,\n                \"_attrs\": attrs,\n                \"_updated_attrs\": {},\n                \"_module\": importlib.import_module(self.__module__),\n            }\n        )\n        self.__dict__[\"_parent_attrs\"] = self.manager.parent_attrs\n        self._create_managers()\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        module = state.pop(\"_module\")\n        state[\"_module_name\"] = module.__name__\n        return state\n\n    def __setstate__(self, state):\n        module_name = state.pop(\"_module_name\")\n        self.__dict__.update(state)\n        self.__dict__[\"_module\"] = importlib.import_module(module_name)\n\n    def __getattr__(self, name):\n        try:\n            return self.__dict__[\"_updated_attrs\"][name]\n        except KeyError:\n            try:\n                value = self.__dict__[\"_attrs\"][name]\n\n                # If the value is a list, we copy it in the _updated_attrs dict\n                # because we are not able to detect changes made on the object\n                # (append, insert, pop, ...). Without forcing the attr\n                # creation __setattr__ is never called, the list never ends up\n                # in the _updated_attrs dict, and the update() and save()\n                # method never push the new data to the server.\n                # See https://github.com/python-gitlab/python-gitlab/issues/306\n                #\n                # note: _parent_attrs will only store simple values (int) so we\n                # don't make this check in the next except block.\n                if isinstance(value, list):\n                    self.__dict__[\"_updated_attrs\"][name] = value[:]\n                    return self.__dict__[\"_updated_attrs\"][name]\n\n                return value\n\n            except KeyError:\n                try:\n                    return self.__dict__[\"_parent_attrs\"][name]\n                except KeyError:\n                    raise AttributeError(name)\n\n    def __setattr__(self, name, value):\n        self.__dict__[\"_updated_attrs\"][name] = value\n\n    def __str__(self):\n        data = self._attrs.copy()\n        data.update(self._updated_attrs)\n        return \"%s => %s\" % (type(self), data)\n\n    def __repr__(self):\n        if self._id_attr:\n            return \"<%s %s:%s>\" % (\n                self.__class__.__name__,\n                self._id_attr,\n                self.get_id(),\n            )\n        else:\n            return \"<%s>\" % self.__class__.__name__\n\n    def __eq__(self, other):\n        if self.get_id() and other.get_id():\n            return self.get_id() == other.get_id()\n        return super(RESTObject, self) == other\n\n    def __ne__(self, other):\n        if self.get_id() and other.get_id():\n            return self.get_id() != other.get_id()\n        return super(RESTObject, self) != other\n\n    def __dir__(self):\n        return super(RESTObject, self).__dir__() + list(self.attributes)\n\n    def __hash__(self):\n        if not self.get_id():\n            return super(RESTObject, self).__hash__()\n        return hash(self.get_id())\n\n    def _create_managers(self):\n        managers = getattr(self, \"_managers\", None)\n        if managers is None:\n            return\n\n        for attr, cls_name in self._managers:\n            cls = getattr(self._module, cls_name)\n            manager = cls(self.manager.gitlab, parent=self)\n            self.__dict__[attr] = manager\n\n    def _update_attrs(self, new_attrs):\n        self.__dict__[\"_updated_attrs\"] = {}\n        self.__dict__[\"_attrs\"].update(new_attrs)\n\n    def get_id(self):\n        \"\"\"Returns the id of the resource.\"\"\"\n        if self._id_attr is None or not hasattr(self, self._id_attr):\n            return None\n        return getattr(self, self._id_attr)\n\n    @property\n    def attributes(self):\n        d = self.__dict__[\"_updated_attrs\"].copy()\n        d.update(self.__dict__[\"_attrs\"])\n        d.update(self.__dict__[\"_parent_attrs\"])\n        return d\n\n\nclass RESTObjectList(object):\n    \"\"\"Generator object representing a list of RESTObject's.\n\n    This generator uses the Gitlab pagination system to fetch new data when\n    required.\n\n    Note: you should not instanciate such objects, they are returned by calls\n    to RESTManager.list()\n\n    Args:\n        manager: Manager to attach to the created objects\n        obj_cls: Type of objects to create from the json data\n        _list: A GitlabList object\n    \"\"\"\n\n    def __init__(self, manager, obj_cls, _list):\n        \"\"\"Creates an objects list from a GitlabList.\n\n        You should not create objects of this type, but use managers list()\n        methods instead.\n\n        Args:\n            manager: the RESTManager to attach to the objects\n            obj_cls: the class of the created objects\n            _list: the GitlabList holding the data\n        \"\"\"\n        self.manager = manager\n        self._obj_cls = obj_cls\n        self._list = _list\n\n    def __iter__(self):\n        return self\n\n    def __len__(self):\n        return len(self._list)\n\n    def __next__(self):\n        return self.next()\n\n    def next(self):\n        data = self._list.next()\n        return self._obj_cls(self.manager, data)\n\n    @property\n    def current_page(self):\n        \"\"\"The current page number.\"\"\"\n        return self._list.current_page\n\n    @property\n    def prev_page(self):\n        \"\"\"The previous page number.\n\n        If None, the current page is the first.\n        \"\"\"\n        return self._list.prev_page\n\n    @property\n    def next_page(self):\n        \"\"\"The next page number.\n\n        If None, the current page is the last.\n        \"\"\"\n        return self._list.next_page\n\n    @property\n    def per_page(self):\n        \"\"\"The number of items per page.\"\"\"\n        return self._list.per_page\n\n    @property\n    def total_pages(self):\n        \"\"\"The total number of pages.\"\"\"\n        return self._list.total_pages\n\n    @property\n    def total(self):\n        \"\"\"The total number of items.\"\"\"\n        return self._list.total\n\n\nclass RESTManager(object):\n    \"\"\"Base class for CRUD operations on objects.\n\n    Derived class must define ``_path`` and ``_obj_cls``.\n\n    ``_path``: Base URL path on which requests will be sent (e.g. '/projects')\n    ``_obj_cls``: The class of objects that will be created\n    \"\"\"\n\n    _path = None\n    _obj_cls = None\n\n    def __init__(self, gl, parent=None):\n        \"\"\"REST manager constructor.\n\n        Args:\n            gl (Gitlab): :class:`~gitlab.Gitlab` connection to use to make\n                         requests.\n            parent: REST object to which the manager is attached.\n        \"\"\"\n        self.gitlab = gl\n        self._parent = parent  # for nested managers\n        self._computed_path = self._compute_path()\n\n    @property\n    def parent_attrs(self):\n        return self._parent_attrs\n\n    def _compute_path(self, path=None):\n        self._parent_attrs = {}\n        if path is None:\n            path = self._path\n        if self._parent is None or not hasattr(self, \"_from_parent_attrs\"):\n            return path\n\n        data = {\n            self_attr: getattr(self._parent, parent_attr, None)\n            for self_attr, parent_attr in self._from_parent_attrs.items()\n        }\n        self._parent_attrs = data\n        return path % data\n\n    @property\n    def path(self):\n        return self._computed_path\n", "path": "gitlab/base.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n#\n# Copyright (C) 2013-2017 Gauvain Pocentek <gauvain@pocentek.net>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU Lesser General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU Lesser General Public License for more details.\n#\n# You should have received a copy of the GNU Lesser General Public License\n# along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nimport importlib\n\n\nclass RESTObject(object):\n    \"\"\"Represents an object built from server data.\n\n    It holds the attributes know from the server, and the updated attributes in\n    another. This allows smart updates, if the object allows it.\n\n    You can redefine ``_id_attr`` in child classes to specify which attribute\n    must be used as uniq ID. ``None`` means that the object can be updated\n    without ID in the url.\n    \"\"\"\n\n    _id_attr = \"id\"\n\n    def __init__(self, manager, attrs):\n        self.__dict__.update(\n            {\n                \"manager\": manager,\n                \"_attrs\": attrs,\n                \"_updated_attrs\": {},\n                \"_module\": importlib.import_module(self.__module__),\n            }\n        )\n        self.__dict__[\"_parent_attrs\"] = self.manager.parent_attrs\n        self._create_managers()\n\n    def __getstate__(self):\n        state = self.__dict__.copy()\n        module = state.pop(\"_module\")\n        state[\"_module_name\"] = module.__name__\n        return state\n\n    def __setstate__(self, state):\n        module_name = state.pop(\"_module_name\")\n        self.__dict__.update(state)\n        self.__dict__[\"_module\"] = importlib.import_module(module_name)\n\n    def __getattr__(self, name):\n        try:\n            return self.__dict__[\"_updated_attrs\"][name]\n        except KeyError:\n            try:\n                value = self.__dict__[\"_attrs\"][name]\n\n                # If the value is a list, we copy it in the _updated_attrs dict\n                # because we are not able to detect changes made on the object\n                # (append, insert, pop, ...). Without forcing the attr\n                # creation __setattr__ is never called, the list never ends up\n                # in the _updated_attrs dict, and the update() and save()\n                # method never push the new data to the server.\n                # See https://github.com/python-gitlab/python-gitlab/issues/306\n                #\n                # note: _parent_attrs will only store simple values (int) so we\n                # don't make this check in the next except block.\n                if isinstance(value, list):\n                    self.__dict__[\"_updated_attrs\"][name] = value[:]\n                    return self.__dict__[\"_updated_attrs\"][name]\n\n                return value\n\n            except KeyError:\n                try:\n                    return self.__dict__[\"_parent_attrs\"][name]\n                except KeyError:\n                    raise AttributeError(name)\n\n    def __setattr__(self, name, value):\n        self.__dict__[\"_updated_attrs\"][name] = value\n\n    def __str__(self):\n        data = self._attrs.copy()\n        data.update(self._updated_attrs)\n        return \"%s => %s\" % (type(self), data)\n\n    def __repr__(self):\n        if self._id_attr:\n            return \"<%s %s:%s>\" % (\n                self.__class__.__name__,\n                self._id_attr,\n                self.get_id(),\n            )\n        else:\n            return \"<%s>\" % self.__class__.__name__\n\n    def __eq__(self, other):\n        if self.get_id() and other.get_id():\n            return self.get_id() == other.get_id()\n        return super(RESTObject, self) == other\n\n    def __ne__(self, other):\n        if self.get_id() and other.get_id():\n            return self.get_id() != other.get_id()\n        return super(RESTObject, self) != other\n\n    def __dir__(self):\n        return super(RESTObject, self).__dir__() + list(self.attributes)\n\n    def __hash__(self):\n        if not self.get_id():\n            return super(RESTObject, self).__hash__()\n        return hash(self.get_id())\n\n    def _create_managers(self):\n        managers = getattr(self, \"_managers\", None)\n        if managers is None:\n            return\n\n        for attr, cls_name in self._managers:\n            cls = getattr(self._module, cls_name)\n            manager = cls(self.manager.gitlab, parent=self)\n            self.__dict__[attr] = manager\n\n    def _update_attrs(self, new_attrs):\n        self.__dict__[\"_updated_attrs\"] = {}\n        self.__dict__[\"_attrs\"] = new_attrs\n\n    def get_id(self):\n        \"\"\"Returns the id of the resource.\"\"\"\n        if self._id_attr is None or not hasattr(self, self._id_attr):\n            return None\n        return getattr(self, self._id_attr)\n\n    @property\n    def attributes(self):\n        d = self.__dict__[\"_updated_attrs\"].copy()\n        d.update(self.__dict__[\"_attrs\"])\n        d.update(self.__dict__[\"_parent_attrs\"])\n        return d\n\n\nclass RESTObjectList(object):\n    \"\"\"Generator object representing a list of RESTObject's.\n\n    This generator uses the Gitlab pagination system to fetch new data when\n    required.\n\n    Note: you should not instanciate such objects, they are returned by calls\n    to RESTManager.list()\n\n    Args:\n        manager: Manager to attach to the created objects\n        obj_cls: Type of objects to create from the json data\n        _list: A GitlabList object\n    \"\"\"\n\n    def __init__(self, manager, obj_cls, _list):\n        \"\"\"Creates an objects list from a GitlabList.\n\n        You should not create objects of this type, but use managers list()\n        methods instead.\n\n        Args:\n            manager: the RESTManager to attach to the objects\n            obj_cls: the class of the created objects\n            _list: the GitlabList holding the data\n        \"\"\"\n        self.manager = manager\n        self._obj_cls = obj_cls\n        self._list = _list\n\n    def __iter__(self):\n        return self\n\n    def __len__(self):\n        return len(self._list)\n\n    def __next__(self):\n        return self.next()\n\n    def next(self):\n        data = self._list.next()\n        return self._obj_cls(self.manager, data)\n\n    @property\n    def current_page(self):\n        \"\"\"The current page number.\"\"\"\n        return self._list.current_page\n\n    @property\n    def prev_page(self):\n        \"\"\"The previous page number.\n\n        If None, the current page is the first.\n        \"\"\"\n        return self._list.prev_page\n\n    @property\n    def next_page(self):\n        \"\"\"The next page number.\n\n        If None, the current page is the last.\n        \"\"\"\n        return self._list.next_page\n\n    @property\n    def per_page(self):\n        \"\"\"The number of items per page.\"\"\"\n        return self._list.per_page\n\n    @property\n    def total_pages(self):\n        \"\"\"The total number of pages.\"\"\"\n        return self._list.total_pages\n\n    @property\n    def total(self):\n        \"\"\"The total number of items.\"\"\"\n        return self._list.total\n\n\nclass RESTManager(object):\n    \"\"\"Base class for CRUD operations on objects.\n\n    Derived class must define ``_path`` and ``_obj_cls``.\n\n    ``_path``: Base URL path on which requests will be sent (e.g. '/projects')\n    ``_obj_cls``: The class of objects that will be created\n    \"\"\"\n\n    _path = None\n    _obj_cls = None\n\n    def __init__(self, gl, parent=None):\n        \"\"\"REST manager constructor.\n\n        Args:\n            gl (Gitlab): :class:`~gitlab.Gitlab` connection to use to make\n                         requests.\n            parent: REST object to which the manager is attached.\n        \"\"\"\n        self.gitlab = gl\n        self._parent = parent  # for nested managers\n        self._computed_path = self._compute_path()\n\n    @property\n    def parent_attrs(self):\n        return self._parent_attrs\n\n    def _compute_path(self, path=None):\n        self._parent_attrs = {}\n        if path is None:\n            path = self._path\n        if self._parent is None or not hasattr(self, \"_from_parent_attrs\"):\n            return path\n\n        data = {\n            self_attr: getattr(self._parent, parent_attr, None)\n            for self_attr, parent_attr in self._from_parent_attrs.items()\n        }\n        self._parent_attrs = data\n        return path % data\n\n    @property\n    def path(self):\n        return self._computed_path\n", "path": "gitlab/base.py"}]}