diff --git a/features/overrides.feature b/features/overrides.feature index 4343cdb1..103a43ac 100644 --- a/features/overrides.feature +++ b/features/overrides.feature @@ -2,13 +2,13 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys Scenario: Override configured editor with built-in input === editor:'' Given we use the config "tiny.yaml" - When we run jrnl with --config-override editor:'' + When we run jrnl with --config-override editor '' Then the stdin prompt must be launched @skip_win Scenario: Override configured linewrap with a value of 23 Given we use the config "tiny.yaml" - When we run "jrnl -2 --config-override linewrap:23 --format fancy" + When we run "jrnl -2 --config-override linewrap 23 --format fancy" Then the output should be """ @@ -30,13 +30,13 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys @skip_win Scenario: Override color selections with runtime overrides Given we use the config "tiny.yaml" - When we run jrnl with -1 --config-override colors.body:blue + When we run jrnl with -1 --config-override colors.body blue Then the runtime config should have colors.body set to blue @skip_win Scenario: Apply multiple config overrides Given we use the config "tiny.yaml" - When we run jrnl with -1 --config-override colors.body:green,editor:"nano" + When we run jrnl with -1 --config-override colors.body green --config-override editor "nano" Then the runtime config should have colors.body set to green And the runtime config should have editor set to nano @@ -44,7 +44,7 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys @skip_win Scenario Outline: Override configured editor Given we use the config "tiny.yaml" - When we run jrnl with --config-override editor:"" + When we run jrnl with --config-override editor "" Then the editor should have been called Examples: Editor Commands | editor | diff --git a/jrnl/args.py b/jrnl/args.py index 046d29a8..fa76ecbe 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -17,20 +17,8 @@ from .plugins import IMPORT_FORMATS from .plugins import util -def deserialize_config_args(input: str) -> dict: - """Convert a delimited list of configuration key-value pairs into a flat dict - - Example: - An input of - `colors.title: blue, display_format: json, colors.date: green` - will return - ```json - { - 'colors.title': 'blue', - 'display_format': 'json', - 'colors.date': 'green' - } - ``` +def deserialize_config_args(input: list) -> dict: + """Convert a two-element list of configuration key-value pair into a flat dict Args: input (str): list of configuration keys in dot-notation and their respective values. @@ -38,29 +26,30 @@ def deserialize_config_args(input: str) -> dict: Returns: dict: A single level dict of the configuration keys in dot-notation and their respective desired values """ - slug_delimiter = "," - key_value_separator = ":" - _kvpairs = _split_at_delimiter( - input, slug_delimiter, " " - ) # Strip away all whitespace in input, not just leading + assert len(input) == 2 runtime_modifications = {} - for _p in _kvpairs: - l, r = _split_at_delimiter(_p, key_value_separator) - r = r.strip() - if r.isdigit(): - r = int(r) - elif r.lower() == "true": - r = True - elif r.lower() == "false": - r = False - runtime_modifications[l] = r + + l = input[0] + r = input[1] + r = r.strip() + if r.isdigit(): + r = int(r) + elif r.lower() == "true": + r = True + elif r.lower() == "false": + r = False + runtime_modifications[l] = r return runtime_modifications -def _split_at_delimiter( - input: str, slug_delimiter: str, whitespace_to_strip=None -) -> list: - return input.strip(whitespace_to_strip).split(slug_delimiter) +class ConfigurationAction(argparse.Action): + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + + def __call__(self, parser, namespace, values, option_strings=None) -> None: + cfg_overrides = getattr(namespace, self.dest, []) + cfg_overrides.append(deserialize_config_args(values)) + setattr(namespace, self.dest, cfg_overrides) class WrappingFormatter(argparse.RawTextHelpFormatter): @@ -361,25 +350,25 @@ def parse_args(args=[]): ) config_overrides = parser.add_argument_group( - "Config file overrides", - textwrap.dedent("These are one-off overrides of the config file options"), + "Config file override", + textwrap.dedent("Apply a one-off override of the config file option"), ) config_overrides.add_argument( "--config-override", dest="config_override", - action="store", - type=deserialize_config_args, - nargs="?", - default=None, + action=ConfigurationAction, + type=str, + nargs=2, + default=[], metavar="CONFIG_KV_PAIR", help=""" - Override configured key-value pairs with CONFIG_KV_PAIR for this command invocation only. + Override configured key-value pair with CONFIG_KV_PAIR for this command invocation only. Examples: \n \t - Use a different editor for this jrnl entry, call: \n \t jrnl --config-override editor: "nano" \n \t - Override color selections\n - \t jrnl --config-override colors.body: blue, colors.title: green + \t jrnl --config-override colors.body blue --config-override colors.title green """, ) diff --git a/jrnl/override.py b/jrnl/override.py index 160bcbcf..01904710 100644 --- a/jrnl/override.py +++ b/jrnl/override.py @@ -1,18 +1,19 @@ # import logging -def apply_overrides(overrides: dict, base_config: dict) -> dict: - """Unpack parsed overrides in dot-notation and return the "patched" configuration +def apply_overrides(overrides: list, base_config: dict) -> dict: + """Unpack CLI provided overrides into the configuration tree. - Args: - overrides (dict): Single-level dict of config fields in dot-notation and their desired values - base_config (dict): The "saved" configuration, as read from YAML - - Returns: - dict: Updated configuration with applied overrides, in the format of the loaded configuration + :param overrides: List of configuration key-value pairs collected from the CLI + :type overrides: list + :param base_config: Configuration Loaded from the saved YAML + :type base_config: dict + :return: Configuration to be used during runtime with the overrides applied + :rtype: dict """ config = base_config.copy() - for k in overrides: + for pairs in overrides: + k, v = list(pairs.items())[0] nodes = k.split(".") - config = _recursively_apply(config, nodes, overrides[k]) + config = _recursively_apply(config, nodes, v) return config diff --git a/tests/test_config.py b/tests/test_config.py index dc2650a8..aa1b01ee 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -43,10 +43,10 @@ def test_override_configured_editor( mock_load_or_install.return_value = minimal_config mock_isatty.return_value = True - cli_args = shlex.split('--config-override editor:"nano"') + cli_args = shlex.split('--config-override editor "nano"') parser = parse_args(cli_args) assert parser.config_override.__len__() == 1 - assert "editor" in parser.config_override.keys() + assert {"editor": "nano"} in parser.config_override def mock_editor_launch(editor): print("%s launched! Success!" % editor) @@ -54,7 +54,7 @@ def test_override_configured_editor( with mock.patch.object( jrnl, "_write_in_editor", - side_effect=mock_editor_launch(parser.config_override["editor"]), + side_effect=mock_editor_launch("TODO: replace"), return_value="note_contents", ) as mock_write_in_editor: run(parser) @@ -84,9 +84,9 @@ def test_override_configured_colors( ): mock_load_or_install.return_value = minimal_config - cli_args = shlex.split("--config-override colors.body:blue") + cli_args = shlex.split("--config-override colors.body blue") parser = parse_args(cli_args) - assert "colors.body" in parser.config_override.keys() + assert {"colors.body": "blue"} in parser.config_override with mock.patch.object( jrnl, "_write_in_editor", diff --git a/tests/test_override.py b/tests/test_override.py index b7bc0a4a..c673f304 100644 --- a/tests/test_override.py +++ b/tests/test_override.py @@ -16,24 +16,24 @@ def minimal_config(): def test_apply_override(minimal_config): config = minimal_config.copy() - overrides = {"editor": "nano"} + overrides = [{"editor": "nano"}] config = apply_overrides(overrides, config) assert config["editor"] == "nano" def test_override_dot_notation(minimal_config): cfg = minimal_config.copy() - overrides = {"colors.body": "blue"} + overrides = [{"colors.body": "blue"}] cfg = apply_overrides(overrides=overrides, base_config=cfg) assert cfg["colors"] == {"body": "blue", "date": "green"} def test_multiple_overrides(minimal_config): - overrides = { - "colors.title": "magenta", - "editor": "nano", - "journals.burner": "/tmp/journals/burner.jrnl", - } # as returned by parse_args, saved in parser.config_override + overrides = [ + {"colors.title": "magenta"}, + {"editor": "nano"}, + {"journals.burner": "/tmp/journals/burner.jrnl"}, + ] # as returned by parse_args, saved in parser.config_override cfg = apply_overrides(overrides, minimal_config.copy()) assert cfg["editor"] == "nano" diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index 5c469e47..feb31d07 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -36,7 +36,7 @@ def expected_args(**kwargs): "strict": False, "tags": False, "text": [], - "config_override": None, + "config_override": [], } return {**default_args, **kwargs} @@ -209,26 +209,26 @@ def test_version_alone(): def test_editor_override(): - parsed_args = cli_as_dict('--config-override editor:"nano"') - assert parsed_args == expected_args(config_override={"editor": "nano"}) + parsed_args = cli_as_dict('--config-override editor "nano"') + assert parsed_args == expected_args(config_override=[{"editor": "nano"}]) def test_color_override(): - assert cli_as_dict("--config-override colors.body:blue") == expected_args( - config_override={"colors.body": "blue"} + assert cli_as_dict("--config-override colors.body blue") == expected_args( + config_override=[{"colors.body": "blue"}] ) def test_multiple_overrides(): parsed_args = cli_as_dict( - '--config-override colors.title:green,editor:"nano",journal.scratchpad:"/tmp/scratchpad"' + '--config-override colors.title green --config-override editor "nano" --config-override journal.scratchpad "/tmp/scratchpad"' ) assert parsed_args == expected_args( - config_override={ - "colors.title": "green", - "journal.scratchpad": "/tmp/scratchpad", - "editor": "nano", - } + config_override=[ + {"colors.title": "green"}, + {"editor": "nano"}, + {"journal.scratchpad": "/tmp/scratchpad"}, + ] ) @@ -266,49 +266,27 @@ class TestDeserialization: @pytest.mark.parametrize( "input_str", [ - 'editor:"nano", colors.title:blue, default:"/tmp/egg.txt"', - 'editor:"vi -c startinsert", colors.title:blue, default:"/tmp/egg.txt"', - 'editor:"nano", colors.title:blue, default:"/tmp/eg\ g.txt"', + ["editor", '"nano"'], + ["colors.title", "blue"], + ["default", "/tmp/egg.txt"], ], ) def test_deserialize_multiword_strings(self, input_str): runtime_config = deserialize_config_args(input_str) assert runtime_config.__class__ == dict - assert "editor" in runtime_config.keys() - assert "colors.title" in runtime_config.keys() - assert "default" in runtime_config.keys() - - def test_deserialize_int(self): - input = "linewrap: 23, default_hour: 19" - runtime_config = deserialize_config_args(input) - assert runtime_config["linewrap"] == 23 - assert runtime_config["default_hour"] == 19 + assert input_str[0] in runtime_config.keys() + assert runtime_config[input_str[0]] == input_str[1] def test_deserialize_multiple_datatypes(self): - input = ( - 'linewrap: 23, encrypt: false, editor:"vi -c startinsert", highlight: true' - ) - cfg = deserialize_config_args(input) - assert cfg["encrypt"] == False - assert cfg["highlight"] == True + cfg = deserialize_config_args(["linewrap", "23"]) assert cfg["linewrap"] == 23 + + cfg = deserialize_config_args(["encrypt", "false"]) + assert cfg["encrypt"] == False + + cfg = deserialize_config_args(["editor", '"vi -c startinsert"']) assert cfg["editor"] == '"vi -c startinsert"' - @pytest.mark.parametrize( - "delimiter", - [ - ".", - ":", - ", ", # note the whitespaces - "-|-", # no reason not to handle multi-character delimiters - ], - ) - def test_split_at_delimiter(self, delimiter): - input = delimiter.join( - ["eggs ", "ba con", "ham"] - ) # The whitespaces are deliberate - from jrnl.args import _split_at_delimiter - - assert _split_at_delimiter(input, delimiter) == ["eggs ", "ba con", "ham"] - assert _split_at_delimiter(input, delimiter, " ") == ["eggs ", "ba con", "ham"] + cfg = deserialize_config_args(["highlight", "true"]) + assert cfg["highlight"] == True