Repository: digitalocean/pynetbox
Pull request #210 (2019-12-10 04:52:09)

Fix using Record objects in hashable collections
This PR fixes two dependent issues with Record objects now being hashable:

1. #208 reports  the `name` attribute missing from `Endpoint` objects. b76e09d7bcb11850acb91d1e6da9c4139672b8c6 now includes this as an instance attribute. There's some string (re)formatting of `name`, both in the constructor, which swap underscores for dashes, and in the `_lookup_ret_obj` method, which converts `name` to CamelCase. I went with the former, but open to feedback.

2. PR https://github.com/digitalocean/pynetbox/pull/196 introduced the ability to use Records in hashable collections. However, there is no `__eq__` method in the `Record` class defined, which causes two equal Record objects to be treated as unique since there is no implementation for comparison. This creates an issue when attempting to use Record objects in hashable collections, such as dictionary keys. The unit tests only check for the hash result being equal but do not exercise object comparison. b761930baf048f2d1bed9cfebd7e56e83d6bf194 adds a comparison operator as well as an additional unit test to exercise object comparison.

Repro code:
```
notify = {}                                                  
for c in circuits:                                           
    circuit = nb.circuits.circuits.get(cid=c)                
    provider = circuit.provider                              
    if provider in notify:                                   
        print(f'Duplicate provider {provider} found.')       
    else:                                                    
        print(f'New provider found {provider}. Inserting...')
        notify[provider] = c                                 
```

Without patch set:
```
~/code/notify# ./main.py
New provider found Telia. Inserting...
New provider found NTT. Inserting...
New provider found Telia. Inserting...
New provider found NTT. Inserting...
New provider found Telia. Inserting...
```
With patch set:
```
~/code/notify# ./main.py
New provider found Telia. Inserting...
New provider found NTT. Inserting...
Duplicate provider Telia found.
Duplicate provider NTT found.
Duplicate provider Telia found.
```


Patch:
diff --git a/pynetbox/core/endpoint.py b/pynetbox/core/endpoint.py
index 85ec0a6..8e38e05 100644
--- a/pynetbox/core/endpoint.py
+++ b/pynetbox/core/endpoint.py
@@ -42,6 +42,7 @@ class Endpoint(object):
 
     def __init__(self, api, app, name, model=None):
         self.return_obj = self._lookup_ret_obj(name, model)
+        self.name = name.replace("_", "-")
         self.api = api
         self.base_url = api.base_url
         self.token = api.token
@@ -50,7 +51,7 @@ class Endpoint(object):
         self.url = "{base_url}/{app}/{endpoint}".format(
             base_url=self.base_url,
             app=app.name,
-            endpoint=name.replace("_", "-"),
+            endpoint=self.name,
         )
         self._choices = None
 
diff --git a/pynetbox/core/response.py b/pynetbox/core/response.py
index 56b84ca..57fac7c 100644
--- a/pynetbox/core/response.py
+++ b/pynetbox/core/response.py
@@ -213,11 +213,19 @@ class Record(object):
     def __setstate__(self, d):
         self.__dict__.update(d)
 
-    def __hash__(self):
+    def __key__(self):
         if hasattr(self, "id"):
-            return hash((self.endpoint.name, self.id))
+            return (self.endpoint.name, self.id)
         else:
-            return hash(self.endpoint.name)
+            return (self.endpoint.name)
+
+    def __hash__(self):
+        return hash(self.__key__())
+
+    def __eq__(self, other):
+        if isinstance(other, Record):
+            return self.__key__() == other.__key__()
+        return NotImplemented
 
     def _add_cache(self, item):
         key, value = item


Test patch:
diff --git a/tests/unit/test_response.py b/tests/unit/test_response.py
index 4310d23..eea9c5d 100644
--- a/tests/unit/test_response.py
+++ b/tests/unit/test_response.py
@@ -128,3 +128,14 @@ class RecordTestCase(unittest.TestCase):
         test2 = Record({}, None, endpoint2)
         test2.id = 2
         self.assertNotEqual(hash(test1), hash(test2))
+
+    def test_compare(self):
+        endpoint1 = Mock()
+        endpoint1.name = "test-endpoint"
+        endpoint2 = Mock()
+        endpoint2.name = "test-endpoint"
+        test1 = Record({}, None, endpoint1)
+        test1.id = 1
+        test2 = Record({}, None, endpoint2)
+        test2.id = 1
+        self.assertEqual(test1, test2)