diff --git a/mackup/config.py b/mackup/config.py index ebcf48daa..f9c76f02e 100644 --- a/mackup/config.py +++ b/mackup/config.py @@ -1,6 +1,6 @@ """Package used to manage the .mackup.cfg config file.""" -import os +import logging import os.path from .constants import ( @@ -28,22 +28,23 @@ import ConfigParser as configparser +logger = logging.getLogger(__name__) + + class Config(object): """The Mackup Config class.""" - def __init__(self, filename=None): + def __init__(self, config_path=None): """ Create a Config instance. Args: - filename (str): Optional filename of the config file. If empty, - defaults to MACKUP_CONFIG_FILE + config_path (str): Optional path to a mackup config file. """ - assert isinstance(filename, str) or filename is None # Initialize the parser - self._parser = self._setup_parser(filename) + self._parser = self._setup_parser(config_path) # Do we have an old config file ? self._warn_on_old_config() @@ -131,24 +132,62 @@ def apps_to_sync(self): """ return set(self._apps_to_sync) - def _setup_parser(self, filename=None): + @classmethod + def _resolve_config_path(cls, filename=None): + """ + Resolve the optional, user-supplied path to a Mackup config file. If + none supplied, defaults to looking for MACKUP_CONFIG_FILE in the user's + home directory. + + Returns: + str, or None if filename doesn't exist + """ + file_exists = lambda p: os.path.isfile(p) + + if filename is None: + # use $HOME, instead of pathlib.Path.home, to preserve existing behavior + # (some unit tests rely on monkeypatching that value) + path = os.path.abspath(os.path.join(os.environ["HOME"], MACKUP_CONFIG_FILE)) + if file_exists(path): + return path + else: + logger.warning( + "Default config file {} not found, and no alternative filename given.".format( + path + ) + ) + return None + + possible_paths = [ + os.path.expanduser(filename), + os.path.join(os.environ["HOME"], filename), + os.path.join(os.getcwd(), filename), + ] + # iter() call for Python2 compatibility (filter() returns a list in Python2) + path = os.path.abspath(next(iter(filter(file_exists, possible_paths)), None)) + if path: + return path + else: + logger.warning( + "Config file {} not found! Tried paths: {}".format( + filename, possible_paths + ) + ) + return None + + def _setup_parser(self, config_path=None): """ Configure the ConfigParser instance the way we want it. Args: - filename (str) or None - + config_path (str): Optional path to a mackup config file. Returns: SafeConfigParser """ - assert isinstance(filename, str) or filename is None - - # If we are not overriding the config filename - if not filename: - filename = MACKUP_CONFIG_FILE - parser = configparser.SafeConfigParser(allow_no_value=True) - parser.read(os.path.join(os.path.join(os.environ["HOME"], filename))) + # call will return None if config_path doesn't exist + path = self._resolve_config_path(config_path) or "" + parser.read(path) return parser diff --git a/mackup/mackup.py b/mackup/mackup.py index c60184353..7ed72a01d 100644 --- a/mackup/mackup.py +++ b/mackup/mackup.py @@ -19,9 +19,9 @@ class Mackup(object): """Main Mackup class.""" - def __init__(self): + def __init__(self, filename=None): """Mackup Constructor.""" - self._config = config.Config() + self._config = config.Config(filename) self.mackup_folder = self._config.fullpath self.temp_folder = tempfile.mkdtemp(prefix="mackup_tmp_") diff --git a/mackup/main.py b/mackup/main.py index b07a91224..42aa48be7 100644 --- a/mackup/main.py +++ b/mackup/main.py @@ -13,12 +13,13 @@ mackup --version Options: - -h --help Show this screen. - -f --force Force every question asked to be answered with "Yes". - -r --root Allow mackup to be run as superuser. - -n --dry-run Show steps without executing. - -v --verbose Show additional details. - --version Show version. + -h --help Show this screen. + -c FILE --config=FILE Configuration file to use. + -f --force Force every question asked to be answered with "Yes". + -r --root Allow mackup to be run as superuser. + -n --dry-run Show steps without executing. + -v --verbose Show additional details. + --version Show version. Modes of action: 1. list: display a list of all supported applications. @@ -63,7 +64,7 @@ def main(): # Get the command line arg args = docopt(__doc__, version="Mackup {}".format(VERSION)) - mckp = Mackup() + mckp = Mackup(args["--config"]) app_db = ApplicationsDatabase() def printAppHeader(app_name): diff --git a/tests/config_tests.py b/tests/config_tests.py index 6f5bab59d..03c8d0f93 100644 --- a/tests/config_tests.py +++ b/tests/config_tests.py @@ -1,5 +1,6 @@ import unittest import os.path +import tempfile from mackup.constants import ( ENGINE_DROPBOX, @@ -7,6 +8,7 @@ ENGINE_COPY, ENGINE_ICLOUD, ENGINE_FS, + MACKUP_CONFIG_FILE, ) from mackup.config import Config, ConfigError @@ -14,7 +16,7 @@ class TestConfig(unittest.TestCase): def setUp(self): realpath = os.path.dirname(os.path.realpath(__file__)) - os.environ["HOME"] = os.path.join(realpath, "fixtures") + self.mock_home = os.environ["HOME"] = os.path.join(realpath, "fixtures") def test_config_no_config(self): cfg = Config() @@ -232,3 +234,35 @@ def test_config_apps_to_ignore_and_sync(self): def test_config_old_config(self): self.assertRaises(SystemExit, Config, "mackup-old-config.cfg") + + def test_resolve_config_path_returns_default_path_if_file_exists(self): + pass + + def test_resolve_config_path_returns_none_if_default_config_file_nonexistent(self): + resolved_path = Config._resolve_config_path() + + assert resolved_path is None + + def test_resolve_config_path_performs_tilde_expansion(self): + with tempfile.NamedTemporaryFile(dir=os.path.expanduser("~")) as mock_cfg_file: + mock_filename = os.path.basename(mock_cfg_file.name) + filename = os.path.join("~", mock_filename) + resolved_path = Config._resolve_config_path(filename) + + assert resolved_path == mock_cfg_file.name + + def test_resolve_config_path_relative_to_home_dir(self): + with tempfile.NamedTemporaryFile(dir=self.mock_home) as mock_cfg_file: + mock_filename = os.path.basename(mock_cfg_file.name) + resolved_path = Config._resolve_config_path(mock_filename) + + assert resolved_path == mock_cfg_file.name + + def test_resolves_config_path_relative_to_cwd(self): + cwd = os.getcwd() + with tempfile.NamedTemporaryFile(dir=cwd) as mock_cfg_file: + mock_path = mock_cfg_file.name + mock_filename = os.path.basename(mock_cfg_file.name) + resolved_path = Config._resolve_config_path(mock_filename) + + assert resolved_path == mock_path