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.


Changed files:
src/syft/ast/klass.py, tests/syft/core/pointer/pointer_test.py

diff --git a/src/syft/ast/klass.py b/src/syft/ast/klass.py
index 1b47375aa..a0a7002d4 100644
--- a/src/syft/ast/klass.py
+++ b/src/syft/ast/klass.py
@@ -290,6 +290,16 @@ class Class(Callable):
             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 @@ class Class(Callable):
 
     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:
diff --git a/tests/syft/core/pointer/pointer_test.py b/tests/syft/core/pointer/pointer_test.py
index 0e37b2ecc..796697c3f 100644
--- 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"