Repository: googleapis/python-firestore

Issue:
SERVER_TIMESTAMP should survive deep copies (dataclasses.asdict)
## Problem

Currently `SERVER_TIMESTAMP` does not work if it has been copied via `copy.deepcopy`.
This is especially important for `dataclasses`.

Here is an example script to demonstrate the problem

```python
from google.cloud.firestore_v1.transforms import Sentinel
from google.cloud.firestore_v1.transforms import SERVER_TIMESTAMP
import dataclasses

@dataclasses.dataclass
class Example:
  timestamp: type[SERVER_TIMESTAMP] = SERVER_TIMESTAMP

example = Example()


assert example.timestamp is SERVER_TIMESTAMP

asdict = dataclasses.asdict(example)
assert asdict['timestamp'] is SERVER_TIMESTAMP
```

Currently the second assertion fails because the Sentinel has been deep copied.
```
Traceback (most recent call last):
  File "/Users/michael/dev/rfp-tool/test.py", line 15, in <module>
    assert asdict['timestamp'] is SERVER_TIMESTAMP
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
```

## Solution

The Sentinel class should define dunder `__copy__` and `__deepcopy__` methods.

```
diff --git a/google/cloud/firestore_v1/transforms.py b/google/cloud/firestore_v1/transforms.py
index f1361c9..ef0def8 100644
--- a/google/cloud/firestore_v1/transforms.py
+++ b/google/cloud/firestore_v1/transforms.py
@@ -26,6 +26,15 @@ class Sentinel(object):
     def __repr__(self):
         return "Sentinel: {}".format(self.description)

+    def __copy__(self):
+        # Sentinel identity should be preserved across copies.
+        return self
+
+    def __deepcopy__(self):
+        # Sentinel identity should be preserved across deep copies.
+        return self
+
+

 DELETE_FIELD = Sentinel("Value used to delete a field in a document.")
 ```
 
 I will send a PR attached to this issue.

Comments:

Patch:
diff --git a/google/cloud/firestore_v1/transforms.py b/google/cloud/firestore_v1/transforms.py
index f1361c9..ae061f6 100644
--- a/google/cloud/firestore_v1/transforms.py
+++ b/google/cloud/firestore_v1/transforms.py
@@ -26,6 +26,14 @@ class Sentinel(object):
     def __repr__(self):
         return "Sentinel: {}".format(self.description)
 
+    def __copy__(self):
+        # Sentinel identity should be preserved across copies.
+        return self
+
+    def __deepcopy__(self, memo):
+        # Sentinel identity should be preserved across deep copies.
+        return self
+
 
 DELETE_FIELD = Sentinel("Value used to delete a field in a document.")
 


Test patch:
diff --git a/tests/unit/v1/test_transforms.py b/tests/unit/v1/test_transforms.py
index 218650b..1a46f27 100644
--- a/tests/unit/v1/test_transforms.py
+++ b/tests/unit/v1/test_transforms.py
@@ -114,3 +114,23 @@ def test__numericvalue___eq___same_value():
     inst = _make_numeric_value(value)
     other = _make_numeric_value(value)
     assert inst == other
+
+
+def test__server_timestamp_is_same_after_copy():
+    from google.cloud.firestore_v1.transforms import SERVER_TIMESTAMP
+    import copy
+
+    value = SERVER_TIMESTAMP
+
+    value_copy = copy.copy(value)
+    assert value_copy is value
+
+
+def test__server_timestamp_is_same_after_deepcopy():
+    from google.cloud.firestore_v1.transforms import SERVER_TIMESTAMP
+    import copy
+
+    value = SERVER_TIMESTAMP
+
+    value_copy = copy.deepcopy(value)
+    assert value_copy is value