diff --git a/src/tufup/repo/__init__.py b/src/tufup/repo/__init__.py index f75a5f8..6eef251 100644 --- a/src/tufup/repo/__init__.py +++ b/src/tufup/repo/__init__.py @@ -594,8 +594,9 @@ def save_config(self): """Save current configuration.""" config_file_path = self.get_config_file_path() # make paths relative to current working directory (cwd), - # if possible, otherwise keep absolute paths (note, to avoid - # confusion, using paths other than cwd is discouraged) + # if possible, otherwise keep absolute paths (see #50) + # (note, to avoid confusion, using paths other than cwd is discouraged) + # also enforce posix paths on windows (see #147) temp_config_dict = self.config_dict # note self.config_dict is a property for key in ['repo_dir', 'keys_dir']: try: @@ -605,13 +606,14 @@ def save_config(self): # which are truncated with a tilde, # e.g. c:\Users\RUNNER~1\... pathlib.Path.cwd().resolve() - ) + ).as_posix() except ValueError: logger.warning( f'Saving *absolute* path to config, because the path' f' ({temp_config_dict[key]}) is not relative to cwd' f' ({pathlib.Path.cwd()})' ) + temp_config_dict[key] = temp_config_dict[key].as_posix() # write file config_file_path.write_text( data=json.dumps(temp_config_dict, default=str, sort_keys=True, indent=4), @@ -629,6 +631,12 @@ def load_config(cls) -> dict: logger.warning(f'config file not found: {file_path}') except json.JSONDecodeError: logger.warning(f'config file invalid: {file_path}') + # enforce posix paths (for legacy windows configs loaded on linux, see #147) + for key in ['repo_dir', 'keys_dir']: + value = config_dict.get(key) + if value: + # interpret the value as a windows path (\\ or /) then enforce posix (/) + config_dict[key] = pathlib.PureWindowsPath(value).as_posix() return config_dict @classmethod diff --git a/tests/test_repo.py b/tests/test_repo.py index e3dfce3..88fde46 100644 --- a/tests/test_repo.py +++ b/tests/test_repo.py @@ -1,5 +1,6 @@ import copy import tarfile +import unittest from datetime import date, datetime, timedelta import json import logging @@ -37,7 +38,7 @@ SUFFIX_PUB, SUFFIX_PATCH, ) - +from tufup.utils.platform_specific import ON_WINDOWS mock_input = Mock(return_value='') @@ -545,6 +546,21 @@ def test_save_config(self): # note Path.is_relative_to() is introduced in python 3.9 self.assertFalse(pathlib.Path(config_dict[key]).is_absolute()) + @unittest.skipUnless(condition=ON_WINDOWS, reason='windows only') + def test_save_config_windows_paths(self): + # prepare + kwargs = dict(repo_dir='foo\\repo', keys_dir='bar\\keys') + repo = Repository(app_name='test', **kwargs) + # test + repo.save_config() + self.assertTrue(repo.get_config_file_path().exists()) + config_text = repo.get_config_file_path().read_text() + print(config_text) + config = json.loads(repo.get_config_file_path().read_text()) + for key in kwargs.keys(): + with self.subTest(msg=key): + self.assertEqual(kwargs[key].replace('\\', '/'), config[key]) + def test_load_config(self): # file does not exist self.assertEqual(dict(), Repository.load_config()) @@ -553,6 +569,19 @@ def test_load_config(self): # test self.assertEqual(dict(), Repository.load_config()) + @unittest.skipIf(condition=ON_WINDOWS, reason='posix only') + def test_load_config_windows_paths(self): + # prepare (mix windows paths and posix paths for convenience) + mock_config = dict(repo_dir='foo\\repo', keys_dir='/tmp/bar/keys') + config_path = Repository.get_config_file_path() + config_path.write_text(json.dumps(mock_config)) + print(config_path.read_text()) + # test + config = Repository.load_config() + for key in mock_config.keys(): + with self.subTest(msg=key): + self.assertEqual(mock_config[key].replace('\\', '/'), config[key]) + def test_from_config(self): temp_dir = self.temp_dir_path.resolve() repo_dir_abs = temp_dir / 'repo'