Repository: ipython/traitlets (python)

Issue:
Upgrade from traitlets `5.11.2` to `5.12.0` broke script
## Context
I noticed that a script that we use as part of our CI workflow stopped working with the update from traitlets `5.11.2` to `5.12.0`.

The script in question: https://github.com/awslabs/syne-tune/blob/main/.github/workflows/utils/notebooks2scripts.py#L8

Relevant lines: 
```python
from traitlets.config import Config
from nbconvert.exporters import PythonExporter

c = Config()
c.TagRemovePreprocessor.remove_cell_tags = "ci-skip-cell"
python_exporter = PythonExporter(config=c)
```

The script converts Jupyter notebooks into Python scripts that our CI can execute, to make sure that our notebooks work correctly. For more about our use-case, refer to the PR that added it: https://github.com/awslabs/syne-tune/pull/756

We have been using traitlets to skip specific Jupyter notebook cells that have been tagged with a special `ci-skip-cell` tag ([example](https://github.com/awslabs/syne-tune/blob/main/examples/notebooks/tune_xgboost.ipynb?short_path=9a34c5a#L22)). The reason for skipping specific cells is because we found that attempting to convert notebooks with `%pip install` cells did not work. What we are doing is instead installing the dependencies ahead of time in our CI environment, and then running the rest of the converted script.

<img width="358" alt="Screenshot 2023-10-30 at 11 20 53 PM" src="https://github.com/ipython/traitlets/assets/1182047/a58984f0-a556-4e22-ac28-bc90a6a74b53">

## Problem

Before the update to `5.12.0`, running the conversion script would skip the cells tagged with `ci-skip-cell`, however after the update, the cells have stopped being skipped. 

Converted script before updating:
```
#!/usr/bin/env python
# coding: utf-8

# # Tune XGBoost

# ### Install dependencies
# 

# In[ ]:

... (truncated)
```

Converted notebook after the update: 

```
#!/usr/bin/env python
# coding: utf-8

# # Tune XGBoost

# ### Install dependencies
# 

# In[ ]:


get_ipython().run_line_magic('pip', "install 'syne-tune[basic]'")
get_ipython().run_line_magic('pip', 'install xgboost')


# In[ ]:


... (truncated)
```

Note the two additional `get_ipython` lines that we do not want to see. 

PR reverting to `5.11.2` makes all of the test pass again: https://github.com/awslabs/syne-tune/pull/782

Example test run where they were failing: https://github.com/awslabs/syne-tune/actions/runs/6693658169

## Question

Is the new traitlets behavior expected?





Pull request description:
Fix assigning string value to Set
Assigning a string value to a `Set` trait used to implicitly cast to a set containing the string, but it accidentally was changed to become the set of the string's characters after #883. Fix this and add a unit test.

Fixes #887 #891.

Patch:
diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py
index 1d1675a..ecd0d7c 100644
--- a/traitlets/traitlets.py
+++ b/traitlets/traitlets.py
@@ -3698,12 +3698,7 @@ class Set(Container[t.Set[t.Any]]):
 
     def set(self, obj: t.Any, value: t.Any) -> None:
         if isinstance(value, str):
-            return super().set(
-                obj,
-                set(
-                    value,
-                ),
-            )
+            return super().set(obj, {value})
         else:
             return super().set(obj, value)
 


Test patch:
diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py
index dfcf3f0..f9f623b 100644
--- a/tests/test_traitlets.py
+++ b/tests/test_traitlets.py
@@ -1658,6 +1658,24 @@ class TestList(TraitTestBase):
         return value
 
 
+class SetTrait(HasTraits):
+    value = Set(Unicode())
+
+
+class TestSet(TraitTestBase):
+    obj = SetTrait()
+
+    _default_value: t.Set[str] = set()
+    _good_values = [{"a", "b"}, "ab"]
+    _bad_values = [1]
+
+    def coerce(self, value):
+        if isinstance(value, str):
+            # compatibility handling: convert string to set containing string
+            value = {value}
+        return value
+
+
 class Foo:
     pass