Repository: glotzerlab/signac

Issue:
Consider removing config APIs
Currently signac allows users to interact with the config via both Python APIs and the CLI. While this model makes sense for most of signac, it is generally unnecessary for configs. In fact, changes like glotzerlab/signac-flow#573 are moving us towards completely decoupling the configuration logic and just sharing the file. We should investigate whether this approach is viable for signac-flow/other plugin compatibility (i.e. if packages built on top of signac could just rely on configobj directly), in which case we could remove config APIs entirely from signac.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/glotzerlab/signac/pull/814?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab) Report
> Merging [#814](https://codecov.io/gh/glotzerlab/signac/pull/814?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab) (dfd020f) into [master](https://codecov.io/gh/glotzerlab/signac/commit/3ffa9609a78ae1eaab0f33c51aa885e107fcd12c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab) (3ffa960) will **decrease** coverage by `7.73%`.
> The diff coverage is `62.50%`.

```diff
@@            Coverage Diff             @@
##           master     #814      +/-   ##
==========================================
- Coverage   78.07%   70.33%   -7.74%     
==========================================
  Files          66       66              
  Lines        7269     7274       +5     
  Branches     1591     1591              
==========================================
- Hits         5675     5116     -559     
- Misses       1283     1889     +606     
+ Partials      311      269      -42     
```


| [Impacted Files](https://codecov.io/gh/glotzerlab/signac/pull/814?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab) | Coverage Δ | |
|---|---|---|
| [signac/\_\_main\_\_.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL19fbWFpbl9fLnB5) | `65.40% <50.00%> (-6.57%)` | :arrow_down: |
| [signac/common/config.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvbW1vbi9jb25maWcucHk=) | `65.28% <63.63%> (-12.98%)` | :arrow_down: |
| [signac/contrib/project.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvbnRyaWIvcHJvamVjdC5weQ==) | `79.68% <100.00%> (-5.53%)` | :arrow_down: |
| [signac/db/database.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2RiL2RhdGFiYXNlLnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [signac/core/h5store.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvcmUvaDVzdG9yZS5weQ==) | `27.05% <0.00%> (-64.05%)` | :arrow_down: |
| [signac/core/dict\_manager.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvcmUvZGljdF9tYW5hZ2VyLnB5) | `33.72% <0.00%> (-54.66%)` | :arrow_down: |
| [signac/synced\_collections/numpy\_utils.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL3N5bmNlZF9jb2xsZWN0aW9ucy9udW1weV91dGlscy5weQ==) | `64.00% <0.00%> (-36.00%)` | :arrow_down: |
| [signac/common/host.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvbW1vbi9ob3N0LnB5) | `5.59% <0.00%> (-35.67%)` | :arrow_down: |
| [signac/common/validate.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvbW1vbi92YWxpZGF0ZS5weQ==) | `39.13% <0.00%> (-34.79%)` | :arrow_down: |
| [signac/common/connection.py](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab#diff-c2lnbmFjL2NvbW1vbi9jb25uZWN0aW9uLnB5) | `4.87% <0.00%> (-34.15%)` | :arrow_down: |
| ... and [25 more](https://codecov.io/gh/glotzerlab/signac/pull/814/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=glotzerlab)


Patch:
diff --git a/changelog.txt b/changelog.txt
index 19c56bb6..6736b372 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -35,7 +35,7 @@ Deprecated
  - ``JobSearchIndex`` class is deprecated (#600).
  - ``index`` argument is deprecated in ``Project`` methods (#602, #588).
  - ``signac.cite`` module is deprecated (#611, #592).
- - ``config.get_config`` method is deprecated (#675).
+ - The ``config`` module and all its methods are deprecated (#675, #753, #814).
  - Accessing ``Project.workspace`` as a method, it should be accessed as a property (#685).
  - ``Project.num_jobs`` (#685).
  - ``ProjectSchema.__call__``, ``ProjectSchema.detect`` (#685).
diff --git a/signac/__main__.py b/signac/__main__.py
index 0f157a74..6acdf944 100644
--- a/signac/__main__.py
+++ b/signac/__main__.py
@@ -840,13 +840,13 @@ def main_config_show(args):
         for fn in config.CONFIG_FILENAMES:
             if os.path.isfile(fn):
                 if cfg is None:
-                    cfg = config.read_config_file(fn)
+                    cfg = config._read_config_file(fn)
                 else:
-                    cfg.merge(config.read_config_file(fn))
+                    cfg.merge(config._read_config_file(fn))
     elif args.globalcfg:
-        cfg = config.read_config_file(config.FN_CONFIG)
+        cfg = config._read_config_file(config.FN_CONFIG)
     else:
-        cfg = config.load_config()
+        cfg = config._load_config()
     if cfg is None:
         if args.local and args.globalcfg:
             mode = " local or global "
@@ -879,13 +879,13 @@ def main_config_verify(args):
         for fn in config.CONFIG_FILENAMES:
             if os.path.isfile(fn):
                 if cfg is None:
-                    cfg = config.read_config_file(fn)
+                    cfg = config._read_config_file(fn)
                 else:
-                    cfg.merge(config.read_config_file(fn))
+                    cfg.merge(config._read_config_file(fn))
     elif args.globalcfg:
-        cfg = config.read_config_file(config.FN_CONFIG)
+        cfg = config._read_config_file(config.FN_CONFIG)
     else:
-        cfg = config.load_config()
+        cfg = config._load_config()
     if cfg is None:
         if args.local and args.globalcfg:
             mode = " local or global "
@@ -920,7 +920,7 @@ def main_config_set(args):
             "to specify which configuration to modify."
         )
     try:
-        cfg = config.read_config_file(fn_config)
+        cfg = config._read_config_file(fn_config)
     except OSError:
         cfg = config._get_config(fn_config)
     keys = args.key.split(".")
@@ -971,7 +971,7 @@ def main_config_host(args):
             "to specify which configuration to modify."
         )
     try:
-        cfg = config.read_config_file(fn_config)
+        cfg = config._read_config_file(fn_config)
     except OSError:
         cfg = config._get_config(fn_config)
 
diff --git a/signac/common/config.py b/signac/common/config.py
index a70e522e..8411ebcc 100644
--- a/signac/common/config.py
+++ b/signac/common/config.py
@@ -104,8 +104,7 @@ def check_and_fix_permissions(filename):
             logger.debug("Fixed permissions.")
 
 
-def read_config_file(filename):
-    """Read a configuration file."""
+def _read_config_file(filename):
     logger.debug(f"Reading config file '{filename}'.")
     try:
         config = Config(filename, configspec=cfg.split("\n"))
@@ -124,6 +123,20 @@ def read_config_file(filename):
     return config
 
 
+@deprecated(
+    deprecated_in="1.8",
+    removed_in="2.0",
+    current_version=__version__,
+    details=(
+        "The read_config_file method is deprecated. Configs should only be "
+        "accessed via a Project instance.",
+    ),
+)
+def read_config_file(filename):
+    """Read a configuration file."""
+    return _read_config_file(filename)
+
+
 def _get_config(infile=None, configspec=None, *args, **kwargs):
     """Get configuration from a file."""
     if configspec is None:
@@ -135,29 +148,33 @@ def _get_config(infile=None, configspec=None, *args, **kwargs):
     deprecated_in="1.8",
     removed_in="2.0",
     current_version=__version__,
-    details="The get_config method is deprecated. Use read_config_file instead.",
+    details=(
+        "The get_config method is deprecated. Configs should only be "
+        "accessed via a Project instance.",
+    ),
 )
 def get_config(infile=None, configspec=None, *args, **kwargs):  # noqa: D103
+    """Get configuration from a file."""
     return _get_config(infile, configspec, *args, **kwargs)
 
 
-def load_config(root=None, local=False):
+def _load_config(root=None, local=False):
     """Load configuration, searching upward from a root path."""
     if root is None:
         root = os.getcwd()
     config = Config(configspec=cfg.split("\n"))
     if local:
         for fn in _search_local(root):
-            tmp = read_config_file(fn)
+            tmp = _read_config_file(fn)
             config.merge(tmp)
             if "project" in tmp:
                 config["project_dir"] = os.path.dirname(fn)
                 break
     else:
         for fn in _search_standard_dirs():
-            config.merge(read_config_file(fn))
+            config.merge(_read_config_file(fn))
         for fn in search_tree(root):
-            tmp = read_config_file(fn)
+            tmp = _read_config_file(fn)
             config.merge(tmp)
             if "project" in tmp:
                 config["project_dir"] = os.path.dirname(fn)
@@ -165,6 +182,20 @@ def load_config(root=None, local=False):
     return config
 
 
+@deprecated(
+    deprecated_in="1.8",
+    removed_in="2.0",
+    current_version=__version__,
+    details=(
+        "The load_config method is deprecated. Configs should only be "
+        "accessed via a Project instance.",
+    ),
+)
+def load_config(root=None, local=False):
+    """Load configuration, searching upward from a root path."""
+    return _load_config(root, local)
+
+
 class Config(ConfigObj):
     """Manages configuration for a signac project."""
 
diff --git a/signac/contrib/project.py b/signac/contrib/project.py
index e6d7fb33..d8eec04e 100644
--- a/signac/contrib/project.py
+++ b/signac/contrib/project.py
@@ -24,7 +24,7 @@ from threading import RLock
 
 from packaging import version
 
-from ..common.config import Config, _get_config, load_config
+from ..common.config import Config, _get_config, _load_config
 from ..common.deprecation import deprecated
 from ..core.h5store import H5StoreManager
 from ..sync import sync_projects
@@ -269,7 +269,7 @@ class Project:
 
     def __init__(self, config=None):
         if config is None:
-            config = load_config()
+            config = _load_config()
         self._config = _ProjectConfig(
             config, _mutate_hook=partial(_invalidate_config_cache, self)
         )
@@ -2393,7 +2393,7 @@ class Project:
                 f"Unable to determine project id for nonexistent path '{os.path.abspath(root)}'."
             )
 
-        config = load_config(root=root, local=False)
+        config = _load_config(root=root, local=False)
         if "project" not in config or (
             not search
             and os.path.realpath(config["project_dir"]) != os.path.realpath(root)


Test patch:
diff --git a/tests/test_job.py b/tests/test_job.py
index 5d579f66..d57261cc 100644
--- a/tests/test_job.py
+++ b/tests/test_job.py
@@ -72,7 +72,7 @@ class TestJobBase:
         self._tmp_pr = os.path.join(self._tmp_dir.name, "pr")
         self._tmp_wd = os.path.join(self._tmp_dir.name, "wd")
         os.mkdir(self._tmp_pr)
-        self.config = signac.common.config.load_config()
+        self.config = signac.common.config._load_config()
         self.project = self.project_class.init_project(
             name="testing_test_project", root=self._tmp_pr, workspace=self._tmp_wd
         )
diff --git a/tests/test_project.py b/tests/test_project.py
index 2e7c0c52..fdce665b 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -2604,7 +2604,7 @@ class TestProjectStoreBase(test_h5store.TestH5StoreBase):
         self._tmp_pr = os.path.join(self._tmp_dir.name, "pr")
         self._tmp_wd = os.path.join(self._tmp_dir.name, "wd")
         os.mkdir(self._tmp_pr)
-        self.config = signac.common.config.load_config()
+        self.config = signac.common.config._load_config()
         self.project = self.project_class.init_project(
             name="testing_test_project", root=self._tmp_pr, workspace=self._tmp_wd
         )
diff --git a/tests/test_shell.py b/tests/test_shell.py
index 4bb4fc13..27702db6 100644
--- a/tests/test_shell.py
+++ b/tests/test_shell.py
@@ -790,17 +790,17 @@ class TestBasicShell:
 
         self.call("python -m signac init my_project".split())
         out = self.call("python -m signac config --local show".split()).strip()
-        cfg = config.read_config_file("signac.rc")
+        cfg = config._read_config_file("signac.rc")
         expected = config.Config(cfg).write()
         assert out.split(os.linesep) == expected
 
         out = self.call("python -m signac config show".split()).strip()
-        cfg = config.load_config()
+        cfg = config._load_config()
         expected = config.Config(cfg).write()
         assert out.split(os.linesep) == expected
 
         out = self.call("python -m signac config --global show".split()).strip()
-        cfg = config.read_config_file(config.FN_CONFIG)
+        cfg = config._read_config_file(config.FN_CONFIG)
         expected = config.Config(cfg).write()
         assert out.split(os.linesep) == expected