Repository: googleapis/google-cloud-python

Issue:
Remove Transaction dependency on Dataset
Instead imply the dataset ID from the mutations added throughout the transaction.

Needed to finish #477



Discussion:
See #447 and #457 as well

@tseaver I'm starting on this. LMK if you would rather do this than #485. IIUC you know more about the `Transaction` class than I do so I'm happy to yield.

Go ahead -- I'm fine with either of them.


Resolving patch:
diff --git a/gcloud/datastore/_implicit_environ.py b/gcloud/datastore/_implicit_environ.py
--- a/gcloud/datastore/_implicit_environ.py
+++ b/gcloud/datastore/_implicit_environ.py
@@ -2,9 +2,6 @@
 
 Acts as a mutable namespace to allow the datastore package to
 imply the current dataset and connection from the enviroment.
-
-Also provides a base class for classes in the `datastore` package
-which could utilize the implicit enviroment.
 """
 
 
@@ -16,15 +13,3 @@
 
 CONNECTION = None
 """Module global to allow persistent implied connection from enviroment."""
-
-
-class _DatastoreBase(object):
-    """Base for all classes in the datastore package.
-
-    Uses the implicit DATASET object as a default dataset attached
-    to the instances being created. Stores the dataset passed in
-    on the protected (i.e. non-public) attribute `_dataset`.
-    """
-
-    def __init__(self, dataset=None):
-        self._dataset = dataset or DATASET
diff --git a/gcloud/datastore/connection.py b/gcloud/datastore/connection.py
--- a/gcloud/datastore/connection.py
+++ b/gcloud/datastore/connection.py
@@ -149,7 +149,7 @@ def mutation(self):
                   (if one exists) or or a new mutation instance.
         """
         if self.transaction():
-            return self.transaction().mutation()
+            return self.transaction().mutation
         else:
             return datastore_pb.Mutation()
 
@@ -358,7 +358,7 @@ def commit(self, dataset_id, mutation_pb):
 
         if self.transaction():
             request.mode = datastore_pb.CommitRequest.TRANSACTIONAL
-            request.transaction = self.transaction().id()
+            request.transaction = self.transaction().id
         else:
             request.mode = datastore_pb.CommitRequest.NON_TRANSACTIONAL
 
@@ -378,11 +378,11 @@ def rollback(self, dataset_id):
         :type dataset_id: string
         :param dataset_id: The dataset to which the transaction belongs.
         """
-        if not self.transaction() or not self.transaction().id():
+        if not self.transaction() or not self.transaction().id:
             raise ValueError('No transaction to rollback.')
 
         request = datastore_pb.RollbackRequest()
-        request.transaction = self.transaction().id()
+        request.transaction = self.transaction().id
         # Nothing to do with this response, so just execute the method.
         self._rpc(dataset_id, 'rollback', request,
                   datastore_pb.RollbackResponse)
@@ -549,7 +549,7 @@ def _set_read_options(self, request, eventual):
         if eventual:
             opts.read_consistency = datastore_pb.ReadOptions.EVENTUAL
         elif transaction:
-            opts.transaction = transaction.id()
+            opts.transaction = transaction.id
 
 
 def _copy_deferred_keys(lookup_request, lookup_response):
diff --git a/gcloud/datastore/transaction.py b/gcloud/datastore/transaction.py
--- a/gcloud/datastore/transaction.py
+++ b/gcloud/datastore/transaction.py
@@ -18,7 +18,7 @@
 from gcloud.datastore import datastore_v1_pb2 as datastore_pb
 
 
-class Transaction(_implicit_environ._DatastoreBase):
+class Transaction(object):
     """An abstraction representing datastore Transactions.
 
     Transactions can be used to build up a bulk mutuation as well as
@@ -121,44 +121,56 @@ class Transaction(_implicit_environ._DatastoreBase):
       ...   with dataset2.transaction():
       ...     dataset2.entity('Thing').save()
 
-    :type dataset: :class:`gcloud.datastore.dataset.Dataset`
-    :param dataset: The dataset to which this :class:`Transaction` belongs.
+    :type dataset_id: :class:`str`.
+    :param dataset_id: The ID of the dataset.
+
+    :type connection: :class:`gcloud.datastore.connection.Connection`
+    :param connection: The connection used to connect to datastore.
+
+    :raises: :class:`ValueError` if either a connection or dataset ID
+             are not set.
     """
 
-    def __init__(self, dataset=None):
-        super(Transaction, self).__init__(dataset=dataset)
-        # If self._dataset is None, using this transaction will fail.
+    def __init__(self, dataset_id=None, connection=None):
+        self._connection = connection or _implicit_environ.CONNECTION
+        self._dataset_id = dataset_id or _implicit_environ.DATASET_ID
+
+        if self._connection is None or self._dataset_id is None:
+            raise ValueError('A transaction must have a connection and '
+                             'a dataset ID set.')
+
         self._id = None
         self._mutation = datastore_pb.Mutation()
         self._auto_id_entities = []
 
-    def connection(self):
-        """Getter for current connection over which the transaction will run.
+    @property
+    def dataset_id(self):
+        """Getter for dataset ID in which the transaction will run.
 
-        :rtype: :class:`gcloud.datastore.connection.Connection`
-        :returns: The connection over which the transaction will run.
+        :rtype: :class:`str`
+        :returns: The dataset ID in which the transaction will run.
         """
+        return self._dataset_id
 
-        return self.dataset().connection()
-
-    def dataset(self):
-        """Getter for the current dataset.
+    @property
+    def connection(self):
+        """Getter for connection over which the transaction will run.
 
-        :rtype: :class:`gcloud.datastore.dataset.Dataset`
-        :returns: The dataset to which the transaction belongs.
+        :rtype: :class:`gcloud.datastore.connection.Connection`
+        :returns: The connection over which the transaction will run.
         """
+        return self._connection
 
-        return self._dataset
-
+    @property
     def id(self):
         """Getter for the transaction ID.
 
         :rtype: string
         :returns: The ID of the current transaction.
         """
-
         return self._id
 
+    @property
     def mutation(self):
         """Getter for the current mutation.
 
@@ -196,8 +208,8 @@ def begin(self):
         statement, however it can be called explicitly if you don't want
         to use a context manager.
         """
-        self._id = self.connection().begin_transaction(self.dataset().id())
-        self.connection().transaction(self)
+        self._id = self.connection.begin_transaction(self._dataset_id)
+        self.connection.transaction(self)
 
     def rollback(self):
         """Rolls back the current transaction.
@@ -207,8 +219,8 @@ def rollback(self):
         - Sets the current connection's transaction reference to None.
         - Sets the current transaction's ID to None.
         """
-        self.connection().rollback(self.dataset().id())
-        self.connection().transaction(None)
+        self.connection.rollback(self._dataset_id)
+        self.connection.transaction(None)
         self._id = None
 
     def commit(self):
@@ -226,18 +238,18 @@ def commit(self):
         """
         # It's possible that they called commit() already, in which case
         # we shouldn't do any committing of our own.
-        if self.connection().transaction():
-            result = self.connection().commit(self.dataset().id(),
-                                              self.mutation())
+        if self.connection.transaction():
+            result = self.connection.commit(self._dataset_id,
+                                            self.mutation)
 
             # For any of the auto-id entities, make sure we update their keys.
             for i, entity in enumerate(self._auto_id_entities):
                 key_pb = result.insert_auto_id_key[i]
                 new_id = key_pb.path_element[-1].id
-                entity.key(entity.key().completed_key(new_id))
+                entity.key = entity.key.completed_key(new_id)
 
         # Tell the connection that the transaction is over.
-        self.connection().transaction(None)
+        self.connection.transaction(None)
 
         # Clear our own ID in case this gets accidentally reused.
         self._id = None


Test patch:
diff --git a/gcloud/datastore/test_connection.py b/gcloud/datastore/test_connection.py
--- a/gcloud/datastore/test_connection.py
+++ b/gcloud/datastore/test_connection.py
@@ -203,8 +203,8 @@ class Mutation(object):
             pass
 
         class Xact(object):
-            def mutation(self):
-                return Mutation()
+            mutation = Mutation()
+
         conn = self._makeOne()
         conn.transaction(Xact())
         found = conn.mutation()
@@ -765,8 +765,8 @@ def test_commit_w_transaction(self):
         from gcloud.datastore import datastore_v1_pb2 as datastore_pb
 
         class Xact(object):
-            def id(self):
-                return 'xact'
+            id = 'xact'
+
         DATASET_ID = 'DATASET'
         key_pb = self._make_key_pb(DATASET_ID)
         rsp_pb = datastore_pb.CommitResponse()
@@ -808,9 +808,8 @@ def test_rollback_wo_existing_transaction(self):
     def test_rollback_w_existing_transaction_no_id(self):
 
         class Xact(object):
+            id = None
 
-            def id(self):
-                return None
         DATASET_ID = 'DATASET'
         conn = self._makeOne()
         conn.transaction(Xact())
@@ -823,9 +822,8 @@ def test_rollback_ok(self):
         TRANSACTION = 'xact'
 
         class Xact(object):
+            id = TRANSACTION
 
-            def id(self):
-                return TRANSACTION
         rsp_pb = datastore_pb.RollbackResponse()
         conn = self._makeOne()
         conn.transaction(Xact())
@@ -1038,11 +1036,9 @@ def test_save_entity_wo_transaction_w_auto_id(self):
     def test_save_entity_w_transaction(self):
         from gcloud.datastore import datastore_v1_pb2 as datastore_pb
 
-        mutation = datastore_pb.Mutation()
-
         class Xact(object):
-            def mutation(self):
-                return mutation
+            mutation = datastore_pb.Mutation()
+
         DATASET_ID = 'DATASET'
         key_pb = self._make_key_pb(DATASET_ID)
         rsp_pb = datastore_pb.CommitResponse()
@@ -1059,11 +1055,9 @@ def test_save_entity_w_transaction_nested_entity(self):
         from gcloud.datastore import datastore_v1_pb2 as datastore_pb
         from gcloud.datastore.entity import Entity
 
-        mutation = datastore_pb.Mutation()
-
         class Xact(object):
-            def mutation(self):
-                return mutation
+            mutation = datastore_pb.Mutation()
+
         DATASET_ID = 'DATASET'
         nested = Entity()
         nested['bar'] = u'Bar'
@@ -1114,11 +1108,9 @@ def test_delete_entities_wo_transaction(self):
     def test_delete_entities_w_transaction(self):
         from gcloud.datastore import datastore_v1_pb2 as datastore_pb
 
-        mutation = datastore_pb.Mutation()
-
         class Xact(object):
-            def mutation(self):
-                return mutation
+            mutation = datastore_pb.Mutation()
+
         DATASET_ID = 'DATASET'
         key_pb = self._make_key_pb(DATASET_ID)
         rsp_pb = datastore_pb.CommitResponse()
@@ -1162,6 +1154,7 @@ class Transaction(object):
     def __init__(self, id):
         self._id = id
 
+    @property
     def id(self):
         return self._id
 
diff --git a/gcloud/datastore/test_entity.py b/gcloud/datastore/test_entity.py
--- a/gcloud/datastore/test_entity.py
+++ b/gcloud/datastore/test_entity.py
@@ -21,11 +21,19 @@
 
 class TestEntity(unittest2.TestCase):
 
-    def _getTargetClass(self):
+    def setUp(self):
         from gcloud.datastore import _implicit_environ
-        from gcloud.datastore.entity import Entity
+        self._replaced_dataset = _implicit_environ.DATASET
+        self._replaced_dataset_id = _implicit_environ.DATASET_ID
+        _implicit_environ.DATASET = _implicit_environ.DATASET_ID = None
+
+    def tearDown(self):
+        from gcloud.datastore import _implicit_environ
+        _implicit_environ.DATASET = self._replaced_dataset
+        _implicit_environ.DATASET_ID = self._replaced_dataset_id
 
-        _implicit_environ.DATASET = None
+    def _getTargetClass(self):
+        from gcloud.datastore.entity import Entity
         return Entity
 
     def _makeOne(self, key=None, exclude_from_indexes=()):
diff --git a/gcloud/datastore/test_helpers.py b/gcloud/datastore/test_helpers.py
--- a/gcloud/datastore/test_helpers.py
+++ b/gcloud/datastore/test_helpers.py
@@ -20,11 +20,13 @@ class Test_entity_from_protobuf(unittest2.TestCase):
     def setUp(self):
         from gcloud.datastore import _implicit_environ
         self._replaced_dataset = _implicit_environ.DATASET
-        _implicit_environ.DATASET = None
+        self._replaced_dataset_id = _implicit_environ.DATASET_ID
+        _implicit_environ.DATASET = _implicit_environ.DATASET_ID = None
 
     def tearDown(self):
         from gcloud.datastore import _implicit_environ
         _implicit_environ.DATASET = self._replaced_dataset
+        _implicit_environ.DATASET_ID = self._replaced_dataset_id
 
     def _callFUT(self, val):
         from gcloud.datastore.helpers import entity_from_protobuf
diff --git a/gcloud/datastore/test_key.py b/gcloud/datastore/test_key.py
--- a/gcloud/datastore/test_key.py
+++ b/gcloud/datastore/test_key.py
@@ -20,6 +20,16 @@ class TestKey(unittest2.TestCase):
     def setUp(self):
         self._DEFAULT_DATASET = 'DATASET'
 
+        from gcloud.datastore import _implicit_environ
+        self._replaced_dataset = _implicit_environ.DATASET
+        self._replaced_dataset_id = _implicit_environ.DATASET_ID
+        _implicit_environ.DATASET = _implicit_environ.DATASET_ID = None
+
+    def tearDown(self):
+        from gcloud.datastore import _implicit_environ
+        _implicit_environ.DATASET = self._replaced_dataset
+        _implicit_environ.DATASET_ID = self._replaced_dataset_id
+
     def _getTargetClass(self):
         from gcloud.datastore import _implicit_environ
         from gcloud.datastore.dataset import Dataset
diff --git a/gcloud/datastore/test_transaction.py b/gcloud/datastore/test_transaction.py
--- a/gcloud/datastore/test_transaction.py
+++ b/gcloud/datastore/test_transaction.py
@@ -22,72 +22,85 @@ def _getTargetClass(self):
 
         return Transaction
 
-    def _makeOne(self, dataset=None):
-        return self._getTargetClass()(dataset)
+    def _makeOne(self, dataset_id=None, connection=None):
+        return self._getTargetClass()(dataset_id=dataset_id,
+                                      connection=connection)
+
+    def test_ctor_missing_required(self):
+        from gcloud.datastore import _implicit_environ
+
+        self.assertEqual(_implicit_environ.DATASET, None)
+
+        with self.assertRaises(ValueError):
+            self._makeOne()
+        with self.assertRaises(ValueError):
+            self._makeOne(dataset_id=object())
+        with self.assertRaises(ValueError):
+            self._makeOne(connection=object())
 
     def test_ctor(self):
         from gcloud.datastore.datastore_v1_pb2 import Mutation
 
         _DATASET = 'DATASET'
         connection = _Connection()
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
-        self.assertTrue(xact.dataset() is dataset)
-        self.assertEqual(xact.id(), None)
-        self.assertTrue(isinstance(xact.mutation(), Mutation))
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
+        self.assertEqual(xact.dataset_id, _DATASET)
+        self.assertEqual(xact.connection, connection)
+        self.assertEqual(xact.id, None)
+        self.assertTrue(isinstance(xact.mutation, Mutation))
         self.assertEqual(len(xact._auto_id_entities), 0)
-        self.assertTrue(xact.connection() is connection)
 
     def test_ctor_with_env(self):
-        SENTINEL_VAL = object()
-
+        from gcloud._testing import _Monkey
         from gcloud.datastore import _implicit_environ
-        _implicit_environ.DATASET = SENTINEL_VAL
 
-        transaction = self._makeOne(dataset=None)
-        self.assertEqual(transaction.dataset(), SENTINEL_VAL)
+        DATASET_ID = 'DATASET'
+        CONNECTION = _Connection()
+
+        with _Monkey(_implicit_environ, DATASET_ID=DATASET_ID,
+                     CONNECTION=CONNECTION):
+            transaction = self._makeOne()
+
+        self.assertEqual(transaction.dataset_id, DATASET_ID)
+        self.assertEqual(transaction.connection, CONNECTION)
 
     def test_add_auto_id_entity(self):
         entity = _Entity()
         _DATASET = 'DATASET'
         connection = _Connection()
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact.add_auto_id_entity(entity)
         self.assertEqual(xact._auto_id_entities, [entity])
 
     def test_begin(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact.begin()
-        self.assertEqual(xact.id(), 234)
+        self.assertEqual(xact.id, 234)
         self.assertEqual(connection._begun, _DATASET)
         self.assertTrue(connection._xact is xact)
 
     def test_rollback(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact.begin()
         xact.rollback()
-        self.assertEqual(xact.id(), None)
+        self.assertEqual(xact.id, None)
         self.assertEqual(connection._rolled_back, _DATASET)
         self.assertEqual(connection._xact, None)
 
     def test_commit_no_auto_ids(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact._mutation = mutation = object()
         xact.begin()
         xact.commit()
         self.assertEqual(connection._committed, (_DATASET, mutation))
         self.assertTrue(connection._xact is None)
-        self.assertEqual(xact.id(), None)
+        self.assertEqual(xact.id, None)
 
     def test_commit_w_auto_ids(self):
         _DATASET = 'DATASET'
@@ -96,8 +109,7 @@ def test_commit_w_auto_ids(self):
         connection = _Connection(234)
         connection._commit_result = _CommitResult(
             _make_key(_KIND, _ID, _DATASET))
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         entity = _Entity()
         xact.add_auto_id_entity(entity)
         xact._mutation = mutation = object()
@@ -105,57 +117,54 @@ def test_commit_w_auto_ids(self):
         xact.commit()
         self.assertEqual(connection._committed, (_DATASET, mutation))
         self.assertTrue(connection._xact is None)
-        self.assertEqual(xact.id(), None)
-        self.assertEqual(entity._key._path, [{'kind': _KIND, 'id': _ID}])
+        self.assertEqual(xact.id, None)
+        self.assertEqual(entity.key.path, [{'kind': _KIND, 'id': _ID}])
 
     def test_commit_w_already(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact._mutation = object()
         xact.begin()
         connection.transaction(())  # Simulate previous commit via false-ish.
         xact.commit()
         self.assertEqual(connection._committed, None)
         self.assertTrue(connection._xact is None)
-        self.assertEqual(xact.id(), None)
+        self.assertEqual(xact.id, None)
 
     def test_context_manager_no_raise(self):
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact._mutation = mutation = object()
         with xact:
-            self.assertEqual(xact.id(), 234)
+            self.assertEqual(xact.id, 234)
             self.assertEqual(connection._begun, _DATASET)
             self.assertTrue(connection._xact is xact)
         self.assertEqual(connection._committed, (_DATASET, mutation))
         self.assertTrue(connection._xact is None)
-        self.assertEqual(xact.id(), None)
+        self.assertEqual(xact.id, None)
 
     def test_context_manager_w_raise(self):
         class Foo(Exception):
             pass
         _DATASET = 'DATASET'
         connection = _Connection(234)
-        dataset = _Dataset(_DATASET, connection)
-        xact = self._makeOne(dataset)
+        xact = self._makeOne(dataset_id=_DATASET, connection=connection)
         xact._mutation = object()
         try:
             with xact:
-                self.assertEqual(xact.id(), 234)
+                self.assertEqual(xact.id, 234)
                 self.assertEqual(connection._begun, _DATASET)
                 self.assertTrue(connection._xact is xact)
                 raise Foo()
         except Foo:
-            self.assertEqual(xact.id(), None)
+            self.assertEqual(xact.id, None)
             self.assertEqual(connection._rolled_back, _DATASET)
             self.assertEqual(connection._xact, None)
         self.assertEqual(connection._committed, None)
         self.assertTrue(connection._xact is None)
-        self.assertEqual(xact.id(), None)
+        self.assertEqual(xact.id, None)
 
 
 def _make_key(kind, id, dataset_id):
@@ -169,19 +178,6 @@ def _make_key(kind, id, dataset_id):
     return key
 
 
-class _Dataset(object):
-
-    def __init__(self, id, connection=None):
-        self._id = id
-        self._connection = connection
-
-    def id(self):
-        return self._id
-
-    def connection(self):
-        return self._connection
-
-
 class _Connection(object):
     _marker = object()
     _begun = _rolled_back = _committed = _xact = None
@@ -217,9 +213,4 @@ class _Entity(object):
 
     def __init__(self):
         from gcloud.datastore.key import Key
-        self._key = Key('KIND', dataset_id='DATASET')
-
-    def key(self, key=None):
-        if key is not None:
-            self._key = key
-        return self._key
+        self.key = Key('KIND', dataset_id='DATASET')