Repository: gcaufield/MonkeyPack

Issue:
Add support for ini configuration
## Describe the feature
Allow for projects to add an mbgetcfg.ini file to their project to change the configuration without needing to pass parameters on the command line.


Discussion:

Patch:
diff --git a/mbget/config.py b/mbget/config.py
index dffaefb..3771e1a 100644
--- a/mbget/config.py
+++ b/mbget/config.py
@@ -29,38 +29,60 @@
 # ############################################################################ #
 
 import os
+from configparser import ConfigParser
 from typing import Optional
 
 
 class Config(object):
+    __DEFAULTS__ = {
+        "package": "packages.txt",
+        "directory": ".mbpkg",
+        "jungle": "barrels.jungle",
+        "manifest": "manifest.xml",
+    }
+
     def __init__(self, args):
+        self.__args = args
+        self.__config_file = None
+        if os.path.exists("mbgetcfg.ini"):
+            config = ConfigParser()
+            with open("mbgetcfg.ini", "r") as f:
+                data = f.read()
+
+            config.read_string(data)
+
+            if "mbget" in config:
+                self.__config_file = config["mbget"]
+
+        self.__cached_vals = {}
+
         if args.token is not None:
-            self.__token = args.token[0]
+            self.__token = args.token
         elif "MBGET_GH_TOKEN" in os.environ:
             self.__token = os.environ["MBGET_GH_TOKEN"]
         else:
             self.__token = None
 
-        self.__package = args.package
-        self.__output_dir = args.directory
-        self.__jungle = args.jungle
-
     @property
     def jungle(self) -> str:
-        return self.__jungle
+        return self.__get_cached_config("jungle")
 
     @property
     def package(self) -> str:
-        return self.__package
+        return self.__get_cached_config("package")
 
     @property
     def barrel_dir(self) -> str:
-        return self.__output_dir
+        return self.__get_cached_config("directory")
 
     @property
     def token(self) -> Optional[str]:
         return self.__token
 
+    @property
+    def manifest(self) -> str:
+        return self.__get_cached_config("manifest")
+
     def prepare_project_dir(self) -> None:
         """
         Put the project dir into a state where mbget can assume that all output
@@ -75,8 +97,24 @@ class Config(object):
 
         :return:
         """
-        if not os.path.exists(self.__output_dir):
-            os.mkdir(self.__output_dir)
+        if not os.path.exists(self.barrel_dir):
+            os.mkdir(self.barrel_dir)
+
+    def __get_cached_config(self, param) -> str:
+        if param not in self.__cached_vals:
+            self.__add_config_to_cache(param)
+
+        return self.__cached_vals[param]
+
+    def __add_config_to_cache(self, param):
+        if hasattr(self.__args, param) and getattr(self.__args, param) is not None:
+            val = getattr(self.__args, param)
+        elif self.__config_file is not None and param in self.__config_file:
+            val = self.__config_file[param]
+        else:
+            val = self.__DEFAULTS__[param]
+
+        self.__cached_vals[param] = val
 
     @staticmethod
     def open_file(name, mode, callback) -> None:
diff --git a/mbget/main.py b/mbget/main.py
index e85599c..2ed2980 100755
--- a/mbget/main.py
+++ b/mbget/main.py
@@ -43,7 +43,7 @@ from mbget.update import Update
 def run_update(args):
     config = Config(args)
 
-    manifest = Manifest(args.manifest)
+    manifest = Manifest(config.manifest)
     packages = Packages(config)
     cache = Cache(config)
 
@@ -56,26 +56,17 @@ def run_update(args):
 
 def main():
     parser = argparse.ArgumentParser(description="Connect IQ Package Manager")
-    parser.add_argument("-t", "--token", nargs=1, help="Github API token for requests")
-    parser.add_argument(
-        "-m", "--manifest", default="manifest.xml", help="Specify application manifest"
-    )
-    parser.add_argument(
-        "-p",
-        "--package",
-        default="packages.txt",
-        help="Specify the package map text file",
-    )
-    parser.add_argument(
-        "-j", "--jungle", default="barrels.jungle", help="Barrel Jungle file"
-    )
+    parser.add_argument("-t", "--token", help="Github API token for requests")
+    parser.add_argument("-m", "--manifest", help="Specify application manifest")
+    parser.add_argument("-p", "--package", help="Specify the package map text file")
+    parser.add_argument("-j", "--jungle", help="Barrel Jungle file")
     parser.add_argument(
         "-o",
         "--directory",
-        default=".mbpkg",
         help="Specify directory to store barrels in",
     )
     parser.set_defaults(func=None)
+
     subparsers = parser.add_subparsers()
     update_parser = subparsers.add_parser(
         "update", help="Download and update dependencies"
diff --git a/mbget/manifest.py b/mbget/manifest.py
index 25b7ed2..4bcfad3 100644
--- a/mbget/manifest.py
+++ b/mbget/manifest.py
@@ -28,7 +28,7 @@
 #                                                                              #
 # ############################################################################ #
 
-from typing import TextIO
+from typing import TextIO, Union
 from xml.etree import ElementTree as Et
 
 from mbget.errors import Error
@@ -37,7 +37,7 @@ from mbget.errors import Error
 class Manifest(object):
     ns = {"iq": "http://www.garmin.com/xml/connectiq"}
 
-    def __init__(self, manifest_stream: TextIO):
+    def __init__(self, manifest_stream: Union[TextIO, str]):
         self.root = Et.ElementTree(file=manifest_stream).getroot()
         self.__validate_manifest()
         self.__build_version_map()


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 6d6f12c..ef84667 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -27,7 +27,6 @@
 # IN THE SOFTWARE.                                                             #
 #                                                                              #
 # ############################################################################ #
-
 import unittest
 from unittest.mock import patch, mock_open, Mock
 
@@ -42,21 +41,58 @@ class MicroMock(object):
 class TestConfig(unittest.TestCase):
     @staticmethod
     def __build_args(
-        token=None, package="packages.txt", directory="barrels", jungle="barrels.jungle"
+        token=None, package=None, directory=None, jungle=None, manifest=None
     ):
         return MicroMock(
-            token=token, package=package, directory=directory, jungle=jungle
+            token=token,
+            package=package,
+            directory=directory,
+            jungle=jungle,
+            manifest=manifest,
         )
 
+    @staticmethod
+    def __build_cfg(package=None, directory=None, jungle=None, manifest=None) -> str:
+        rv = "[mbget]\n"
+
+        if package is not None:
+            rv += "package = {}\n".format(package)
+
+        if directory is not None:
+            rv += "directory = {}\n".format(directory)
+
+        if jungle is not None:
+            rv += "jungle = {}\n".format(jungle)
+
+        if manifest is not None:
+            rv += "manifest = {}\n".format(manifest)
+
+        return rv
+
+    def setUp(self):
+        super(TestConfig, self).setUp()
+        pat = patch("os.path.exists")
+        self.__mock_path = pat.start()
+        self.__mock_path.return_value = False
+        self.addCleanup(pat.stop)
+
     def test_can_init_config(self):
         Config(self.__build_args())
 
+    def test_reads_config_file_if_exists(self):
+        self.__mock_path.return_value = True
+        m = mock_open(read_data="")
+        with patch("builtins.open", m):
+            Config(self.__build_args())
+
+        m.assert_called_once_with("mbgetcfg.ini", "r")
+
     def test_token_is_none(self):
         cfg = Config(self.__build_args(token=None))
         self.assertIsNone(cfg.token)
 
     def test_token_is_valid(self):
-        cfg = Config(self.__build_args(token=["12345abc"]))
+        cfg = Config(self.__build_args(token="12345abc"))
         self.assertEqual("12345abc", cfg.token)
 
     def test_token_from_env_if_arg_is_none(self):
@@ -66,21 +102,73 @@ class TestConfig(unittest.TestCase):
 
     def test_token_prioritizes_args_over_env(self):
         with patch.dict("os.environ", {"MBGET_GH_TOKEN": "125aeb"}):
-            cfg = Config(self.__build_args(token=["12345abc"]))
+            cfg = Config(self.__build_args(token="12345abc"))
             self.assertEqual("12345abc", cfg.token)
 
+    def test_default_barrel_dir_is_valid(self):
+        cfg = Config(self.__build_args())
+        self.assertEqual(".mbpkg", cfg.barrel_dir)
+
     def test_barrel_dir_is_valid(self):
         cfg = Config(self.__build_args(directory="barrels"))
         self.assertEqual("barrels", cfg.barrel_dir)
 
-    def test_package_is_valid(self):
-        cfg = Config(self.__build_args(package="packages.txt"))
+    def test_barrel_dir_from_cfg_is_valid(self):
+        self.__mock_path.return_value = True
+        m = mock_open(read_data=self.__build_cfg(directory="barrels"))
+        with patch("builtins.open", m):
+            cfg = Config(self.__build_args())
+
+        self.assertEqual("barrels", cfg.barrel_dir)
+
+    def test_default_package_is_valid(self):
+        cfg = Config(self.__build_args())
         self.assertEqual("packages.txt", cfg.package)
 
-    def test_jungle_is_valid(self):
-        cfg = Config(self.__build_args(jungle="barrels.jungle"))
+    def test_package_is_valid(self):
+        cfg = Config(self.__build_args(package="pck.txt"))
+        self.assertEqual("pck.txt", cfg.package)
+
+    def test_package_from_cfg_is_valid(self):
+        self.__mock_path.return_value = True
+        m = mock_open(read_data=self.__build_cfg(package="pack.txt"))
+        with patch("builtins.open", m):
+            cfg = Config(self.__build_args())
+
+        self.assertEqual("pack.txt", cfg.package)
+
+    def test_default_jungle_is_valid(self):
+        cfg = Config(self.__build_args())
         self.assertEqual("barrels.jungle", cfg.jungle)
 
+    def test_jungle_is_valid(self):
+        cfg = Config(self.__build_args(jungle="brrls.jungle"))
+        self.assertEqual("brrls.jungle", cfg.jungle)
+
+    def test_jungle_from_cfg_is_valid(self):
+        self.__mock_path.return_value = True
+        m = mock_open(read_data=self.__build_cfg(jungle="brl.jungle"))
+        with patch("builtins.open", m):
+            cfg = Config(self.__build_args())
+
+        self.assertEqual("brl.jungle", cfg.jungle)
+
+    def test_default_manifest_is_valid(self):
+        cfg = Config(self.__build_args())
+        self.assertEqual("manifest.xml", cfg.manifest)
+
+    def test_manifest_is_valid(self):
+        cfg = Config(self.__build_args(manifest="mfst.xml"))
+        self.assertEqual("mfst.xml", cfg.manifest)
+
+    def test_manifest_from_cfg_is_valid(self):
+        self.__mock_path.return_value = True
+        m = mock_open(read_data=self.__build_cfg(manifest="man.xml"))
+        with patch("builtins.open", m):
+            cfg = Config(self.__build_args())
+
+        self.assertEqual("man.xml", cfg.manifest)
+
     def test_prepare_builds_output_dir_if_not_exists(self):
         cfg = Config(self.__build_args(directory="barrels"))