Repository: glotzerlab/signac

Issue:
Commas in project names cause unexpected errors
### Description

I was creating a signac project named `oP6-As2(Co,Ni,Fe)_r`. This is problematic because the commas are not understood by the configuration parser.

### To reproduce

```python
>>> import signac
>>> signac.init_project('oP6-As2(Co,Ni,Fe)_r')
Traceback (most recent call last):
  File ".../signac/contrib/project.py", line 1462, in init_project
    project = cls.get_project(root=root, search=False)
  File ".../signac/contrib/project.py", line 1509, in get_project
    "Unable to determine project id for path '{}'.".format(os.path.abspath(root)))
LookupError: Unable to determine project id for path '/tmp_signac'.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../signac/contrib/project.py", line 1867, in init_project
    return Project.init_project(name=name, root=root, workspace=workspace, make_dir=make_dir)
  File ".../signac/contrib/project.py", line 1472, in init_project
    assert project.get_id() == str(name)
AssertionError
```
or
```bash
$ signac init "oP6-As2(Co,Ni,Fe)_r"
Error:
```

In both cases, a file `signac.rc` is created with the contents:
```ini
project=oP6-As2(Co,Ni,Fe)_r
```

The project name is misinterpreted as a list:
```python
>>> project.get_id()
"['oP6-As2(Co', 'Ni', 'Fe)_r']"
```

### System configuration
 - Operating System: macOS
 - Version of Python: 3.7.3
 - Version of signac [e.g. 1.0]: 1.2.0 (running on latest `master`)

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=h1) Report
> Merging [#252](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=desc) into [master](https://codecov.io/gh/glotzerlab/signac/commit/38a31f96c91b8ebc3dc7674935c24b7ec0547bce?src=pr&el=desc) will **increase** coverage by `0.09%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/glotzerlab/signac/pull/252/graphs/tree.svg?width=650&token=FghbjTyzgM&height=150&src=pr)](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #252      +/-   ##
==========================================
+ Coverage   64.96%   65.06%   +0.09%     
==========================================
  Files          39       39              
  Lines        5580     5581       +1     
==========================================
+ Hits         3625     3631       +6     
+ Misses       1955     1950       -5
```


| [Impacted Files](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [signac/contrib/project.py](https://codecov.io/gh/glotzerlab/signac/pull/252/diff?src=pr&el=tree#diff-c2lnbmFjL2NvbnRyaWIvcHJvamVjdC5weQ==) | `90.97% <100%> (+0.01%)` | :arrow_up: |
| [signac/common/config.py](https://codecov.io/gh/glotzerlab/signac/pull/252/diff?src=pr&el=tree#diff-c2lnbmFjL2NvbW1vbi9jb25maWcucHk=) | `70.37% <0%> (+4.62%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=footer). Last update [38a31f9...88ee59e](https://codecov.io/gh/glotzerlab/signac/pull/252?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/changelog.txt b/changelog.txt
index ee46ffa0..e3dfe099 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -34,6 +34,7 @@ Fixed
 +++++
 
  - Attempting to create a linked view for a Project on Windows now raises an informative error message.
+ - Project configuration is initialized using ConfigObj, allowing the configuration to include commas and special characters (#251, #252).
 
 Deprecated
 ++++++++++
diff --git a/signac/contrib/project.py b/signac/contrib/project.py
index 96b7ce8f..c8804173 100644
--- a/signac/contrib/project.py
+++ b/signac/contrib/project.py
@@ -24,7 +24,7 @@ from ..core import json
 from ..core.jsondict import JSONDict
 from ..core.h5store import H5StoreManager
 from .collection import Collection
-from ..common.config import load_config, Config
+from ..common.config import get_config, load_config, Config
 from ..sync import sync_projects
 from .job import Job
 from .hashing import calc_id
@@ -1461,10 +1461,11 @@ class Project(object):
             fn_config = os.path.join(root, 'signac.rc')
             if make_dir:
                 _mkdir_p(os.path.dirname(fn_config))
-            with open(fn_config, 'a') as config_file:
-                config_file.write('project={}\n'.format(name))
-                if workspace is not None:
-                    config_file.write('workspace_dir={}\n'.format(workspace))
+            config = get_config(fn_config)
+            config['project'] = name
+            if workspace is not None:
+                config['workspace_dir'] = workspace
+            config.write()
             project = cls.get_project(root=root)
             assert project.get_id() == str(name)
             return project


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index e34b1b24..2870e2d7 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -9,6 +9,7 @@ import logging
 import itertools
 import json
 import pickle
+import string
 from tarfile import TarFile
 from zipfile import ZipFile
 from tempfile import TemporaryDirectory
@@ -1794,6 +1795,14 @@ class ProjectInitTest(unittest.TestCase):
         self.assertEqual(project.workspace(), os.path.join(root, 'workspace'))
         self.assertEqual(project.root_directory(), root)
 
+    def test_get_project_all_printable_characters(self):
+        root = self._tmp_dir.name
+        with self.assertRaises(LookupError):
+            signac.get_project(root=root)
+        project_name = 'testproject' + string.printable
+        project = signac.init_project(name=project_name, root=root)
+        self.assertEqual(project.get_id(), project_name)
+
     def test_get_project_non_local(self):
         root = self._tmp_dir.name
         subdir = os.path.join(root, 'subdir')