Repository: Anselmoo/spectrafit

Issue:
[Feature]: Input Convert as cmd tool
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Missing Feature

Converting input files from:

1. `json` &rarr; `toml` or `yaml`
2. `toml ` &rarr; `json ` or `yaml`
3. `yaml ` &rarr; `toml` or `toml `

### Possible Solution

_No response_

### Anything else?

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:

Resolving patch:
diff --git a/spectrafit/plugins/__init__.py b/spectrafit/plugins/__init__.py
new file mode 100644
--- /dev/null
+++ b/spectrafit/plugins/__init__.py
@@ -0,0 +1 @@
+"""Plugins for SpectraFit."""
diff --git a/spectrafit/plugins/converter.py b/spectrafit/plugins/converter.py
new file mode 100644
--- /dev/null
+++ b/spectrafit/plugins/converter.py
@@ -0,0 +1,78 @@
+"""Convert the input and output files to the preferred file format."""
+import argparse
+import json
+
+from pathlib import Path
+from typing import Any
+from typing import Dict
+
+import toml
+import yaml
+
+from spectrafit.tools import read_input_file
+
+
+choices = ["json", "yaml", "yml", "toml", "lock"]
+
+
+def get_args() -> Dict[str, Any]:
+    """Get the arguments from the command line.
+
+    Returns:
+        Dict[str, Any]: Return the input file arguments as a dictionary without
+             additional information beyond the command line arguments.
+    """
+    parser = argparse.ArgumentParser(
+        description="Converter for 'SpectraFit' input and output files."
+    )
+    parser.add_argument(
+        "infile", type=Path, help="Filename of the 'SpectraFit' input or output file."
+    )
+    parser.add_argument(
+        "-f",
+        "--format",
+        help="File format for the conversion.",
+        type=str,
+        choices=choices,
+    )
+    return vars(parser.parse_args())
+
+
+def convert(args: Dict[str, Any]) -> None:
+    """Convert the input file to the output file.
+
+    Args:
+        args (Dict[str, Any]): The input file arguments as a dictionary with
+             additional information beyond the command line arguments.
+
+    Raises:
+        ValueError: If the input file format is identical with the output format.
+        ValueError: If the output file format is not supported.
+    """
+    if args["infile"].suffix[1:] == args["format"]:
+        raise ValueError(
+            f"The input file suffix '{args['infile'].suffix[1:]}' is similar to the"
+            f" output file format '{args['format']}'."
+            "Please use a different output file suffix."
+        )
+
+    if args["format"] not in choices:
+        raise ValueError(f"The output file format '{args['format']}' is not supported.")
+
+    data = read_input_file(args["infile"])
+
+    if args["format"] == "json":
+        # Convert the input file to a JSON file
+        with open(args["infile"].with_suffix(".json"), "w", encoding="utf8") as f:
+            json.dump(data, f, indent=4)
+    elif args["format"] == "yaml":
+        with open(args["infile"].with_suffix(".yaml"), "w", encoding="utf8") as f:
+            yaml.dump(data, f, default_flow_style=False)
+    elif args["format"] in ["toml", "lock"]:
+        with open(args["infile"].with_suffix(".toml"), "w", encoding="utf8") as f:
+            toml.dump(data, f)
+
+
+def command_line_runner() -> None:
+    """Run the converter via cmd commands."""
+    convert(get_args())
diff --git a/spectrafit/tools.py b/spectrafit/tools.py
--- a/spectrafit/tools.py
+++ b/spectrafit/tools.py
@@ -451,13 +451,15 @@ def save_as_csv(self) -> None:
     def save_as_json(self) -> None:
         """Save the fitting result as json file."""
         if self.args["outfile"]:
-            with open(Path(f"{self.args['outfile']}_summary.json"), "w") as f:
+            with open(
+                Path(f"{self.args['outfile']}_summary.json"), "w", encoding="utf8"
+            ) as f:
                 json.dump(self.args, f, indent=4)
         else:
             raise FileNotFoundError("No output file provided!")
 
 
-def read_input_file(fname: str) -> MutableMapping[str, Any]:
+def read_input_file(fname: Path) -> MutableMapping[str, Any]:
     """Read the input file.
 
     Read the input file as `toml`, `json`, or `yaml` files and return as a dictionary.
@@ -473,15 +475,15 @@ def read_input_file(fname: str) -> MutableMapping[str, Any]:
              information beyond the command line arguments.
 
     """
-    _fname = Path(fname)
+    fname = Path(fname)
 
-    if _fname.suffix == ".toml":
+    if fname.suffix == ".toml":
         args = toml.load(fname)
-    elif _fname.suffix == ".json":
-        with open(_fname, "r") as f:
+    elif fname.suffix == ".json":
+        with open(fname, "r", encoding="utf8") as f:
             args = json.load(f)
-    elif _fname.suffix in [".yaml", ".yml"]:
-        with open(_fname, "r") as f:
+    elif fname.suffix in [".yaml", ".yml"]:
+        with open(fname, "r", encoding="utf8") as f:
             args = yaml.load(f, Loader=yaml.FullLoader)
     else:
         raise OSError(
@@ -514,7 +516,7 @@ def load_data(args: Dict[str, str]) -> pd.DataFrame:
     try:
         if args["global"]:
             return pd.read_csv(
-                Path(args["infile"]),
+                args["infile"],
                 sep=args["separator"],
                 header=args["header"],
                 dtype=np.float64,
@@ -522,7 +524,7 @@ def load_data(args: Dict[str, str]) -> pd.DataFrame:
                 comment=args["comment"],
             )
         return pd.read_csv(
-            Path(args["infile"]),
+            args["infile"],
             sep=args["separator"],
             header=args["header"],
             usecols=args["column"],


Test patch:
diff --git a/spectrafit/plugins/test/__init__.py b/spectrafit/plugins/test/__init__.py
new file mode 100644
--- /dev/null
+++ b/spectrafit/plugins/test/__init__.py
@@ -0,0 +1 @@
+"""Test of the SpectraFit plugins."""
diff --git a/spectrafit/plugins/test/test_converter.py b/spectrafit/plugins/test/test_converter.py
new file mode 100644
--- /dev/null
+++ b/spectrafit/plugins/test/test_converter.py
@@ -0,0 +1,104 @@
+"""Test of the converter plugin."""
+
+import json
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+import pytest
+import toml
+import yaml
+
+from spectrafit.plugins.converter import convert
+
+
+def test_cmd_converter(script_runner) -> None:
+    """Test the converter plugin."""
+    ret = script_runner.run("spectrafit-converter", "-h")
+
+    assert ret.success
+    assert "Converter for 'SpectraFit' input and output files." in ret.stdout
+    assert ret.stderr == ""
+
+
+def test_raise_input_output():
+    """Test raise error input format is similar to ouptut."""
+    args = {
+        "infile": Path("tests/data/input/input_1.yaml"),
+        "format": "yaml",
+    }
+    with pytest.raises(ValueError) as excinfo:
+        convert(args)
+
+    assert (
+        "The input file suffix 'yaml' is similar to the output file format 'yaml'."
+        in str(excinfo.value)
+    )
+
+
+def test_raise_no_guilty_ouput():
+    """Test illegal output format."""
+    args = {
+        "infile": Path("tests/data/input/input_1.yaml"),
+        "format": "illegal",
+    }
+    with pytest.raises(ValueError) as excinfo:
+        convert(args)
+    assert "The output file format 'illegal' is not supported." in str(excinfo.value)
+
+
+def test_json_conversion():
+    """Test json to yaml conversion."""
+    with TemporaryDirectory() as tmpdir:
+        infile = Path(tmpdir) / "input_1.json"
+
+        # write input json
+        with open(infile, "w", encoding="utf8") as f:
+            json.dump({"a": 1, "b": 2}, f)
+        args = {
+            "infile": infile,
+            "format": "yaml",
+        }
+        convert(args)
+        with open(infile.with_suffix(".yaml"), encoding="utf8") as f:
+            data = yaml.safe_load(f)
+
+        assert data == {"a": 1, "b": 2}
+
+
+def test_yaml_conversion():
+    """Test yaml to json conversion."""
+    with TemporaryDirectory() as tmpdir:
+        infile = Path(tmpdir) / "input_1.yaml"
+
+        # write input yaml
+        with open(infile, "w", encoding="utf8") as f:
+            yaml.dump({"a": 1, "b": 2}, f)
+        args = {
+            "infile": infile,
+            "format": "toml",
+        }
+        convert(args)
+        with open(infile.with_suffix(".toml"), encoding="utf8") as f:
+            data = toml.load(f)
+
+        assert data == {"a": 1, "b": 2}
+
+
+def test_toml_conversion():
+    """Test toml to json conversion."""
+    with TemporaryDirectory() as tmpdir:
+        infile = Path(tmpdir) / "input_1.toml"
+
+        # write input toml
+        with open(infile, "w", encoding="utf8") as f:
+            toml.dump({"a": 1, "b": 2}, f)
+        args = {
+            "infile": infile,
+            "format": "json",
+        }
+        convert(args)
+        with open(infile.with_suffix(".json"), encoding="utf8") as f:
+            data = json.load(f)
+
+        assert data == {"a": 1, "b": 2}