Repository: googleapis/python-bigquery

Issue:
Make AccessEntry hashable
I would like to make AccessEntry hashable. I ran into it being unhashable and could not see a reason why.



Discussion:

Resolving patch:
diff --git a/google/cloud/bigquery/dataset.py b/google/cloud/bigquery/dataset.py
--- a/google/cloud/bigquery/dataset.py
+++ b/google/cloud/bigquery/dataset.py
@@ -20,6 +20,7 @@
 import copy
 
 import google.cloud._helpers
+
 from google.cloud.bigquery import _helpers
 from google.cloud.bigquery.model import ModelReference
 from google.cloud.bigquery.routine import RoutineReference
@@ -145,38 +146,60 @@ def __init__(self, role, entity_type, entity_id):
                     "Role must be set for entity " "type %r" % (entity_type,)
                 )
 
-        self.role = role
-        self.entity_type = entity_type
-        self.entity_id = entity_id
+        self._role = role
+        self._entity_type = entity_type
+        self._entity_id = entity_id
+
+    @property
+    def role(self):
+        """str: The role of the entry."""
+        return self._role
+
+    @property
+    def entity_type(self):
+        """str: The entity_type of the entry."""
+        return self._entity_type
+
+    @property
+    def entity_id(self):
+        """str: The entity_id of the entry."""
+        return self._entity_id
 
     def __eq__(self, other):
         if not isinstance(other, AccessEntry):
             return NotImplemented
-        return (
-            self.role == other.role
-            and self.entity_type == other.entity_type
-            and self.entity_id == other.entity_id
-        )
+        return self._key() == other._key()
 
     def __ne__(self, other):
         return not self == other
 
     def __repr__(self):
         return "<AccessEntry: role=%s, %s=%s>" % (
-            self.role,
-            self.entity_type,
-            self.entity_id,
+            self._role,
+            self._entity_type,
+            self._entity_id,
         )
 
+    def _key(self):
+        """ A tuple key that uniquely describes this field.
+        Used to compute this instance's hashcode and evaluate equality.
+        Returns:
+            Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
+        """
+        return (self._role, self._entity_type, self._entity_id)
+
+    def __hash__(self):
+        return hash(self._key())
+
     def to_api_repr(self):
         """Construct the API resource representation of this access entry
 
         Returns:
             Dict[str, object]: Access entry represented as an API resource
         """
-        resource = {self.entity_type: self.entity_id}
-        if self.role is not None:
-            resource["role"] = self.role
+        resource = {self._entity_type: self._entity_id}
+        if self._role is not None:
+            resource["role"] = self._role
         return resource
 
     @classmethod


Test patch:
diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py
--- a/tests/unit/test_dataset.py
+++ b/tests/unit/test_dataset.py
@@ -84,6 +84,20 @@ def test__eq___type_mismatch(self):
         self.assertNotEqual(entry, object())
         self.assertEqual(entry, mock.ANY)
 
+    def test___hash__set_equality(self):
+        entry1 = self._make_one("OWNER", "userByEmail", "silly@example.com")
+        entry2 = self._make_one("OWNER", "userByEmail", "phred@example.com")
+        set_one = {entry1, entry2}
+        set_two = {entry1, entry2}
+        self.assertEqual(set_one, set_two)
+
+    def test___hash__not_equals(self):
+        entry1 = self._make_one("OWNER", "userByEmail", "silly@example.com")
+        entry2 = self._make_one("OWNER", "userByEmail", "phred@example.com")
+        set_one = {entry1}
+        set_two = {entry2}
+        self.assertNotEqual(set_one, set_two)
+
     def test_to_api_repr(self):
         entry = self._make_one("OWNER", "userByEmail", "salmon@example.com")
         resource = entry.to_api_repr()