From ae009099ed8b69c2f311dc44f68551ee35c2a318 Mon Sep 17 00:00:00 2001 From: samuelgregorovic <47627184+samuelgregorovic@users.noreply.github.com> Date: Sat, 6 Nov 2021 22:12:34 +0100 Subject: [PATCH] Add --config-file argument to use alternate config file at runtime (#1290) * added new CLI argument option --config-file * pass argument and fetch alt config file if specified * argparse argument setting update * argument alias --cf added * documentation update - usage of CLI argument * fixed name-clash + unit tests * feature test added * #1170-alternate-config-file: Auto stash before rebase of "refs/heads/#1170-alternate-config-file" * Update docs/advanced.md Co-authored-by: Jonathan Wren * BDD tests added * Begin migrating/rewording --cf tests in pytest-bdd. Uses current directory instead of deep directory structure, but requires a given for each config file referenced * Fix issue where specifying a config-file that needs to be upgraded ended up upgrading the user config file instead * Uncomment and rework remaining tests for pytest-bdd instead of behave * Fix copytree for Python 3.7 (which doesn't support dirs_exist_ok) * Minor fixes to alternative config examples * Remove behave tests (behave is no longer in use) * Move config file unit test to unit test dir and use pytext path fixture instead of current directory to find test data * Use explicit "given the config exists" for copying config files instead of shoehorning in "given we use the config" twice * Change when/when to when/and * Clarify scenarios and fix indentation * Confirm primary config file isn't modified when encrypting/decrypting a journal in an alternate config file * Remove try/except on copytree since I'm no longer using the same Co-authored-by: Jonathan Wren Co-authored-by: Micah Jerome Ellison --- docs/advanced.md | 19 ++++++ jrnl/args.py | 25 +++++++ jrnl/config.py | 9 ++- jrnl/install.py | 45 +++++++++---- jrnl/jrnl.py | 2 +- tests/bdd/features/config_file.feature | 92 ++++++++++++++++++++++++++ tests/bdd/test_features.py | 1 + tests/lib/given_steps.py | 8 +++ tests/unit/test_config_file.py | 22 ++++++ tests/unit/test_parse_args.py | 1 + 10 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 tests/bdd/features/config_file.feature create mode 100644 tests/unit/test_config_file.py diff --git a/docs/advanced.md b/docs/advanced.md index b1b7bef0..a9f1fb27 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -85,6 +85,25 @@ jrnl --config-override display_format fancy --config-override linewrap 20 \ ``` +### Using an alternate config + +You can specify an alternate configuration file for the current instance of `jrnl` using `--config-file CONFIG_FILE_PATH` where +`CONFIG_FILE_PATH` is a path to an alternate `jrnl` configuration file. + +#### Examples: + +``` +# Use personalised configuration file for personal journal entries +jrnl --config-file ~/foo/jrnl/personal-config.yaml + +# Use alternate configuration file for work-related entries +jrnl --config-file ~/foo/jrnl/work-config.yaml + +# Use default configuration file (created on installation) +jrnl +``` + + ## Multiple journal files You can configure `jrnl`to use with multiple journals (eg. diff --git a/jrnl/args.py b/jrnl/args.py index 972fe802..604f9c0e 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -337,6 +337,31 @@ def parse_args(args=[]): """, ) + alternate_config = parser.add_argument_group( + "Specifies alternate config to be used", + textwrap.dedent("Applies alternate config for current session"), + ) + + alternate_config.add_argument( + "--config-file", + dest="config_file_path", + type=str, + default="", + help=""" + Overrides default (created when first installed) config file for this command only. + + Examples: \n + \t - Use a work config file for this jrnl entry, call: \n + \t jrnl --config-file /home/user1/work_config.yaml + \t - Use a personal config file stored on a thumb drive: \n + \t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml + """, + ) + + alternate_config.add_argument( + "--cf", dest="config_file_path", type=str, default="", help=argparse.SUPPRESS + ) + # Handle '-123' as a shortcut for '-n 123' num = re.compile(r"^-(\d+)$") args = [num.sub(r"-n \1", arg) for arg in args] diff --git a/jrnl/config.py b/jrnl/config.py index a0482405..035fb34a 100644 --- a/jrnl/config.py +++ b/jrnl/config.py @@ -47,9 +47,14 @@ def make_yaml_valid_dict(input: list) -> dict: return runtime_modifications -def save_config(config): +def save_config(config, alt_config_path=None): + """Supply alt_config_path if using an alternate config through --config-file.""" config["version"] = __version__ - with open(get_config_path(), "w", encoding=YAML_FILE_ENCODING) as f: + with open( + alt_config_path if alt_config_path else get_config_path(), + "w", + encoding=YAML_FILE_ENCODING, + ) as f: yaml.safe_dump( config, f, diff --git a/jrnl/install.py b/jrnl/install.py index b0ae2aa1..b2b583cf 100644 --- a/jrnl/install.py +++ b/jrnl/install.py @@ -19,32 +19,53 @@ from .prompt import yesno from .upgrade import is_old_version -def upgrade_config(config): +def upgrade_config(config_data, alt_config_path=None): """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. This essentially automatically ports jrnl installations if new config parameters are introduced in later - versions.""" + versions. + Supply alt_config_path if using an alternate config through --config-file.""" default_config = get_default_config() - missing_keys = set(default_config).difference(config) + missing_keys = set(default_config).difference(config_data) if missing_keys: for key in missing_keys: - config[key] = default_config[key] - save_config(config) + config_data[key] = default_config[key] + save_config(config_data, alt_config_path) + config_path = alt_config_path if alt_config_path else get_config_path() print( - f"[Configuration updated to newest version at {get_config_path()}]", + f"[Configuration updated to newest version at {config_path}]", file=sys.stderr, ) -def load_or_install_jrnl(): - """ - If jrnl is already installed, loads and returns a config object. - Else, perform various prompts to install jrnl. - """ +def find_default_config(): config_path = ( get_config_path() if os.path.exists(get_config_path()) else os.path.join(os.path.expanduser("~"), ".jrnl_config") ) + return config_path + + +def find_alt_config(alt_config): + if os.path.exists(alt_config): + return alt_config + else: + print( + "Alternate configuration file not found at path specified.", file=sys.stderr + ) + print("Exiting.", file=sys.stderr) + sys.exit(1) + + +def load_or_install_jrnl(alt_config_path): + """ + If jrnl is already installed, loads and returns a default config object. + If alternate config is specified via --config-file flag, it will be used. + Else, perform various prompts to install jrnl. + """ + config_path = ( + find_alt_config(alt_config_path) if alt_config_path else find_default_config() + ) if os.path.exists(config_path): logging.debug("Reading configuration from file %s", config_path) @@ -68,7 +89,7 @@ def load_or_install_jrnl(): print("Exiting.", file=sys.stderr) sys.exit(1) - upgrade_config(config) + upgrade_config(config, alt_config_path) verify_config_colors(config) else: diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index bc7e0b88..9d128a3a 100644 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -36,7 +36,7 @@ def run(args): # Load the config, and extract journal name try: - config = install.load_or_install_jrnl() + config = install.load_or_install_jrnl(args.config_file_path) original_config = config.copy() # Apply config overrides diff --git a/tests/bdd/features/config_file.feature b/tests/bdd/features/config_file.feature new file mode 100644 index 00000000..ce4f042b --- /dev/null +++ b/tests/bdd/features/config_file.feature @@ -0,0 +1,92 @@ +Feature: Multiple journals + + Scenario: Read a journal from an alternate config + Given the config "basic_onefile.yaml" exists + And we use the config "multiple.yaml" + When we run "jrnl --cf basic_onefile.yaml -999" + Then the output should not contain "My first entry" # from multiple.yaml + And the output should contain "Lorem ipsum" # from basic_onefile.yaml + + Scenario: Write to default journal by default using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl --cf multiple.yaml this goes to default" + And we run "jrnl -1" + Then the output should not contain "this goes to default" + When we run "jrnl --cf multiple.yaml -1" + Then the output should contain "this goes to default" + + Scenario: Write to specified journal using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl work --cf multiple.yaml a long day in the office" + And we run "jrnl default --cf multiple.yaml -1" + Then the output should contain "But I'm better" + When we run "jrnl work --cf multiple.yaml -1" + Then the output should contain "a long day in the office" + + Scenario: Tell user which journal was used while using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl --cf multiple.yaml work a long day in the office" + Then we should see the message "Entry added to work journal" + + Scenario: Write to specified journal with a timestamp using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl work --cf multiple.yaml 23 july 2012: a long day in the office" + And we run "jrnl --cf multiple.yaml -1" + Then the output should contain "But I'm better" + When we run "jrnl --cf multiple.yaml work -1" + Then the output should contain "a long day in the office" + And the output should contain "2012-07-23" + + Scenario: Write to specified journal without a timestamp but with colon using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl work --cf multiple.yaml : a long day in the office" + And we run "jrnl --cf multiple.yaml -1" + Then the output should contain "But I'm better" + When we run "jrnl --cf multiple.yaml work -1" + Then the output should contain "a long day in the office" + + Scenario: Create new journals as required using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl ideas -1" + Then the output should be empty + When we run "jrnl ideas --cf multiple.yaml 23 july 2012: sell my junk on ebay and make lots of money" + Then the output should contain "Journal 'ideas' created" + When we run "jrnl ideas --cf multiple.yaml -1" + Then the output should contain "sell my junk on ebay and make lots of money" + + Scenario: Don't crash if no default journal is specified using an alternate config + Given the config "bug343.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl --cf bug343.yaml a long day in the office" + Then we should see the message "No default journal configured" + + Scenario: Don't crash if no file exists for a configured encrypted journal using an alternate config + Given the config "multiple.yaml" exists + And we use the config "basic_onefile.yaml" + When we run "jrnl new_encrypted --cf multiple.yaml Adding first entry" and enter + these three eyes + these three eyes + n + Then we should see the message "Encrypted journal 'new_encrypted' created" + + Scenario: Don't overwrite main config when encrypting a journal in an alternate config + Given the config "basic_onefile.yaml" exists + And we use the config "multiple.yaml" + When we run "jrnl --cf basic_onefile.yaml --encrypt" and enter + these three eyes + these three eyes + n + Then we should see the message "Journal encrypted to features/journals/basic_onefile.journal" + And the config should contain "encrypt: false" # multiple.yaml remains unchanged + + Scenario: Don't overwrite main config when decrypting a journal in an alternate config + Given the config "editor_encrypted.yaml" exists + And we use the config "basic_encrypted.yaml" + When we run "jrnl --cf editor_encrypted.yaml --decrypt" + Then the config should contain "encrypt: true" # basic_encrypted remains unchanged diff --git a/tests/bdd/test_features.py b/tests/bdd/test_features.py index 1509e92d..5ef3506e 100644 --- a/tests/bdd/test_features.py +++ b/tests/bdd/test_features.py @@ -1,6 +1,7 @@ from pytest_bdd import scenarios scenarios("features/build.feature") +scenarios("features/config_file.feature") scenarios("features/core.feature") scenarios("features/datetime.feature") scenarios("features/delete.feature") diff --git a/tests/lib/given_steps.py b/tests/lib/given_steps.py index 649d44c5..f3e6b69c 100644 --- a/tests/lib/given_steps.py +++ b/tests/lib/given_steps.py @@ -105,6 +105,14 @@ def we_use_the_config(config_file, temp_dir, working_dir): return config_dest +@given(parse('the config "{config_file}" exists'), target_fixture="config_path") +@given('the config "" exists', target_fixture="config_path") +def config_exists(config_file, temp_dir, working_dir): + config_source = os.path.join(working_dir, "data", "configs", config_file) + config_dest = os.path.join(temp_dir.name, config_file) + shutil.copy2(config_source, config_dest) + + @given(parse('we use the password "{pw}" if prompted'), target_fixture="password") def use_password_forever(pw): return pw diff --git a/tests/unit/test_config_file.py b/tests/unit/test_config_file.py new file mode 100644 index 00000000..04766f4a --- /dev/null +++ b/tests/unit/test_config_file.py @@ -0,0 +1,22 @@ +import pytest +import os + +from jrnl.install import find_alt_config + + +def test_find_alt_config(request): + work_config_path = os.path.join( + request.fspath.dirname, "..", "data", "configs", "basic_onefile.yaml" + ) + found_alt_config = find_alt_config(work_config_path) + assert found_alt_config == work_config_path + + +def test_find_alt_config_not_exist(request): + bad_config_path = os.path.join( + request.fspath.dirname, "..", "data", "configs", "not-existing-config.yaml" + ) + with pytest.raises(SystemExit) as ex: + found_alt_config = find_alt_config(bad_config_path) + assert found_alt_config is not None + assert isinstance(ex.value, SystemExit) diff --git a/tests/unit/test_parse_args.py b/tests/unit/test_parse_args.py index 0725d33d..f408c9aa 100644 --- a/tests/unit/test_parse_args.py +++ b/tests/unit/test_parse_args.py @@ -37,6 +37,7 @@ def expected_args(**kwargs): "tags": False, "text": [], "config_override": [], + "config_file_path": "", } return {**default_args, **kwargs}