Repository: PrefectHQ/prefect

Issue:
Bug: groups and tags don't set correctly after changing to _private context variables
Missed these attributes when changing context to use a private attribute -- need to fix + add tests.


Discussion:

Resolving patch:
diff --git a/prefect/core/task.py b/prefect/core/task.py
--- a/prefect/core/task.py
+++ b/prefect/core/task.py
@@ -71,10 +71,10 @@ def __init__(
 
         self.group = str(group or prefect.context.get("_group", ""))
 
+        # avoid silently iterating over a string
         if isinstance(tags, str):
-            tags = [tags]
-        self.tags = set(tags or [])
-        self.tags.update(prefect.context.get("_tags", []))
+            raise TypeError("Tags should be a set of tags, not a string.")
+        self.tags = set(tags or prefect.context.get("_tags", []))
 
         self.environment = environment
         self.checkpoint = checkpoint
diff --git a/prefect/utilities/tasks.py b/prefect/utilities/tasks.py
--- a/prefect/utilities/tasks.py
+++ b/prefect/utilities/tasks.py
@@ -1,5 +1,5 @@
 from contextlib import contextmanager
-from typing import Any, Callable
+from typing import Any, Callable, Iterator
 
 from toolz import curry
 
@@ -7,27 +7,26 @@
 
 
 @contextmanager
-def group(name: str) -> None:
+def group(name: str, append: bool = False) -> Iterator[None]:
     """
     Context manager for setting a task group.
     """
-    current_group = prefect.context.get("group", "")
-    if current_group:
-        name = current_group + "." + name
-    with prefect.context(group=name):
+    if append:
+        current_group = prefect.context.get("_group", "")
+        if current_group:
+            name = current_group + "/" + name
+    with prefect.context(_group=name):
         yield
 
 
 @contextmanager
-def tags(*tags):
+def tags(*tags: str) -> Iterator[None]:
     """
     Context manager for setting task tags.
     """
-    tags = set(tags)
-    current_tags = prefect.context.get("tags", set())
-    if current_tags:
-        tags = tags.update(current_tags)
-    with prefect.context(tags=tags):
+    tags_set = set(tags)
+    tags_set.update(prefect.context.get("_tags", set()))
+    with prefect.context(_tags=tags_set):
         yield
 
 


Test patch:
diff --git a/tests/core/test_task.py b/tests/core/test_task.py
--- a/tests/core/test_task.py
+++ b/tests/core/test_task.py
@@ -100,8 +100,8 @@ def test_tags():
     t1 = Task()
     assert t1.tags == set()
 
-    t2 = Task(tags="test")
-    assert t2.tags == set(["test"])
+    with pytest.raises(TypeError):
+        Task(tags="test")
 
     t3 = Task(tags=["test", "test2", "test"])
     assert t3.tags == set(["test", "test2"])
@@ -112,7 +112,7 @@ def test_tags():
 
     with prefect.context(_tags=["test1", "test2"]):
         t5 = Task(tags=["test3"])
-        assert t5.tags == set(["test1", "test2", "test3"])
+        assert t5.tags == set(["test3"])
 
 
 def test_inputs():
diff --git a/tests/utilities/test_tasks.py b/tests/utilities/test_tasks.py
--- a/tests/utilities/test_tasks.py
+++ b/tests/utilities/test_tasks.py
@@ -1,15 +1,15 @@
 from prefect.core import Flow, Task
-from prefect.utilities.tasks import task
+from prefect.utilities import tasks
 
 
 def test_task_decorator_can_be_used_without_calling():
-    @task
+    @tasks.task
     def fun(x, y):
         return x + y
 
 
 def test_task_decorator_generates_new_tasks_upon_subsequent_calls():
-    @task
+    @tasks.task
     def fun(x, y):
         return x + y
 
@@ -18,3 +18,52 @@ def fun(x, y):
         res2 = fun(1, 2)
     assert isinstance(res1, Task)
     assert res1 is not res2
+
+
+def test_context_manager_for_setting_group():
+    """
+    Test setting a Task group with a context manager, including:
+        - top level
+        - nested
+        - nested with explicit group passed
+        - nested with append
+        - top level with append
+    """
+    with tasks.group("1"):
+        t1 = Task()
+        assert t1.group == "1"
+
+        with tasks.group("2"):
+            t2 = Task()
+            assert t2.group == "2"
+
+            t3 = Task(group="3")
+            assert t3.group == "3"
+
+        with tasks.group("2", append=True):
+            t4 = Task()
+            assert t4.group == "1/2"
+
+    with tasks.group("1", append=True):
+        t5 = Task()
+        assert t5.group == "1"
+
+
+def test_context_manager_for_setting_tags():
+    """
+    Test setting Task tags with a context manager, including:
+        - top level
+        - nested
+        - nested with explicit tags
+    """
+
+    with tasks.tags("1", "2"):
+        t1 = Task()
+        assert t1.tags == set(["1", "2"])
+
+        with tasks.tags("3", "4"):
+            t2 = Task()
+            assert t2.tags == set(["1", "2", "3", "4"])
+
+            t3 = Task(tags=["5"])
+            assert t3.tags == set(["5"])