Repository: OpenMined/PySyft

Issue:
Re-add .tag() to Pointer
## Description
Until we figure out which way we want to go with the API we should keep the .tag() method to prevent breaking compatibility with existing docs and examples.

For all instances except C types in libs like PSI, users should be able to use either or both of:
```
obj.tag("first").tag("second").send(duet, searchable=True)
```

or
```
obj.send(duet, tags=["first", "second"], searchable=True)
```

If the tags arg is supplied to .send it should overwrite any previous tags, but if its empty we will use the previously set ones.

## Definition of Done
The old functionality of .tag still functions and the PSI .send(tags=[]) style still works as well.
A test which makes sure both of these work as expected is added and passes.



Discussion:

Resolving patch:
diff --git a/src/syft/ast/klass.py b/src/syft/ast/klass.py
--- a/src/syft/ast/klass.py
+++ b/src/syft/ast/klass.py
@@ -290,6 +290,16 @@ def send(
             if id_ is None:
                 id_ = UID()
                 which_obj.id = id_
+
+            tags = sorted(set(tags), key=tags.index)  # keep order of original
+            obj_tags = getattr(which_obj, "tags", [])
+            # if `tags` is passed in, use it; else, use obj_tags
+            tags = tags if tags else obj_tags
+
+            obj_description = getattr(which_obj, "description", "")
+            # if `description` is passed in, use it; else, use obj_description
+            description = description if description else obj_description
+
             which_obj.tags = tags
             which_obj.description = description
 
@@ -324,7 +334,7 @@ def send(
 
     def create_storable_object_attr_convenience_methods(outer_self: Any) -> None:
         def tag(self: Any, *tags: Tuple[Any, ...]) -> object:
-            self.tags = list(tags)
+            self.tags = sorted(set(tags), key=tags.index)  # keep order of original
             return self
 
         def describe(self: Any, description: str) -> object:


Test patch:
diff --git a/tests/syft/core/pointer/pointer_test.py b/tests/syft/core/pointer/pointer_test.py
--- a/tests/syft/core/pointer/pointer_test.py
+++ b/tests/syft/core/pointer/pointer_test.py
@@ -69,3 +69,41 @@ def test_searchable_property() -> None:
 
     ptr.searchable = False
     assert len(client.store) == 0
+
+
+def test_tags() -> None:
+    bob = sy.VirtualMachine(name="Bob")
+    root_client = bob.get_root_client()
+
+    ten = th.tensor([1, 2])
+
+    ten = ten.tag("tag1", "tag1", "other")
+    assert ten.tags == ["tag1", "other"]
+
+    # .send without `tags` passed in
+    ptr = ten.send(root_client)
+    assert ptr.tags == ["tag1", "other"]
+
+    # .send with `tags` passed in
+    ptr = ten.send(root_client, tags=["tag2", "tag2", "other"])
+    assert ten.tags == ["tag2", "other"]
+    assert ptr.tags == ["tag2", "other"]
+
+
+def test_description() -> None:
+    bob = sy.VirtualMachine(name="Bob")
+    root_client = bob.get_root_client()
+
+    ten = th.tensor([1, 2])
+
+    ten = ten.describe("description 1")
+    assert ten.description == "description 1"
+
+    # .send without `description` passed in
+    ptr = ten.send(root_client)
+    assert ptr.description == "description 1"
+
+    # .send with `description` passed in
+    ptr = ten.send(root_client, description="description 2")
+    assert ten.description == "description 2"
+    assert ptr.description == "description 2"