Simplify config override syntax (#5)

* update tests and expected behavior

* clean up arg parsing tests

* update deserialization

* update deserialization

* config argparse action

* update override application logic

* update tests; delete unused imports

* override param must be list

* update docstring

* update test input to SUT

* update remaining override unittests

* make format

* forgot to update CLI syntax
This commit is contained in:
Suhas 2021-01-31 20:17:02 -05:00 committed by GitHub
parent 40f93a9322
commit 6e658c31df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 82 additions and 114 deletions

View file

@ -2,13 +2,13 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
Scenario: Override configured editor with built-in input === editor:'' Scenario: Override configured editor with built-in input === editor:''
Given we use the config "tiny.yaml" 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 Then the stdin prompt must be launched
@skip_win @skip_win
Scenario: Override configured linewrap with a value of 23 Scenario: Override configured linewrap with a value of 23
Given we use the config "tiny.yaml" 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 Then the output should be
""" """
@ -30,13 +30,13 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
@skip_win @skip_win
Scenario: Override color selections with runtime overrides Scenario: Override color selections with runtime overrides
Given we use the config "tiny.yaml" 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 Then the runtime config should have colors.body set to blue
@skip_win @skip_win
Scenario: Apply multiple config overrides Scenario: Apply multiple config overrides
Given we use the config "tiny.yaml" 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 Then the runtime config should have colors.body set to green
And the runtime config should have editor set to nano And the runtime config should have editor set to nano
@ -44,7 +44,7 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys
@skip_win @skip_win
Scenario Outline: Override configured editor Scenario Outline: Override configured editor
Given we use the config "tiny.yaml" Given we use the config "tiny.yaml"
When we run jrnl with --config-override editor:"<editor>" When we run jrnl with --config-override editor "<editor>"
Then the editor <editor> should have been called Then the editor <editor> should have been called
Examples: Editor Commands Examples: Editor Commands
| editor | | editor |

View file

@ -17,20 +17,8 @@ from .plugins import IMPORT_FORMATS
from .plugins import util from .plugins import util
def deserialize_config_args(input: str) -> dict: def deserialize_config_args(input: list) -> dict:
"""Convert a delimited list of configuration key-value pairs into a flat dict """Convert a two-element list of configuration key-value pair 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'
}
```
Args: Args:
input (str): list of configuration keys in dot-notation and their respective values. input (str): list of configuration keys in dot-notation and their respective values.
@ -38,14 +26,11 @@ def deserialize_config_args(input: str) -> dict:
Returns: Returns:
dict: A single level dict of the configuration keys in dot-notation and their respective desired values dict: A single level dict of the configuration keys in dot-notation and their respective desired values
""" """
slug_delimiter = "," assert len(input) == 2
key_value_separator = ":"
_kvpairs = _split_at_delimiter(
input, slug_delimiter, " "
) # Strip away all whitespace in input, not just leading
runtime_modifications = {} runtime_modifications = {}
for _p in _kvpairs:
l, r = _split_at_delimiter(_p, key_value_separator) l = input[0]
r = input[1]
r = r.strip() r = r.strip()
if r.isdigit(): if r.isdigit():
r = int(r) r = int(r)
@ -57,10 +42,14 @@ def deserialize_config_args(input: str) -> dict:
return runtime_modifications return runtime_modifications
def _split_at_delimiter( class ConfigurationAction(argparse.Action):
input: str, slug_delimiter: str, whitespace_to_strip=None def __init__(self, **kwargs) -> None:
) -> list: super().__init__(**kwargs)
return input.strip(whitespace_to_strip).split(slug_delimiter)
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): class WrappingFormatter(argparse.RawTextHelpFormatter):
@ -361,25 +350,25 @@ def parse_args(args=[]):
) )
config_overrides = parser.add_argument_group( config_overrides = parser.add_argument_group(
"Config file overrides", "Config file override",
textwrap.dedent("These are one-off overrides of the config file options"), textwrap.dedent("Apply a one-off override of the config file option"),
) )
config_overrides.add_argument( config_overrides.add_argument(
"--config-override", "--config-override",
dest="config_override", dest="config_override",
action="store", action=ConfigurationAction,
type=deserialize_config_args, type=str,
nargs="?", nargs=2,
default=None, default=[],
metavar="CONFIG_KV_PAIR", metavar="CONFIG_KV_PAIR",
help=""" 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 Examples: \n
\t - Use a different editor for this jrnl entry, call: \n \t - Use a different editor for this jrnl entry, call: \n
\t jrnl --config-override editor: "nano" \n \t jrnl --config-override editor: "nano" \n
\t - Override color selections\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
""", """,
) )

View file

@ -1,18 +1,19 @@
# import logging # import logging
def apply_overrides(overrides: dict, base_config: dict) -> dict: def apply_overrides(overrides: list, base_config: dict) -> dict:
"""Unpack parsed overrides in dot-notation and return the "patched" configuration """Unpack CLI provided overrides into the configuration tree.
Args: :param overrides: List of configuration key-value pairs collected from the CLI
overrides (dict): Single-level dict of config fields in dot-notation and their desired values :type overrides: list
base_config (dict): The "saved" configuration, as read from YAML :param base_config: Configuration Loaded from the saved YAML
:type base_config: dict
Returns: :return: Configuration to be used during runtime with the overrides applied
dict: Updated configuration with applied overrides, in the format of the loaded configuration :rtype: dict
""" """
config = base_config.copy() config = base_config.copy()
for k in overrides: for pairs in overrides:
k, v = list(pairs.items())[0]
nodes = k.split(".") nodes = k.split(".")
config = _recursively_apply(config, nodes, overrides[k]) config = _recursively_apply(config, nodes, v)
return config return config

View file

@ -43,10 +43,10 @@ def test_override_configured_editor(
mock_load_or_install.return_value = minimal_config mock_load_or_install.return_value = minimal_config
mock_isatty.return_value = True 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) parser = parse_args(cli_args)
assert parser.config_override.__len__() == 1 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): def mock_editor_launch(editor):
print("%s launched! Success!" % editor) print("%s launched! Success!" % editor)
@ -54,7 +54,7 @@ def test_override_configured_editor(
with mock.patch.object( with mock.patch.object(
jrnl, jrnl,
"_write_in_editor", "_write_in_editor",
side_effect=mock_editor_launch(parser.config_override["editor"]), side_effect=mock_editor_launch("TODO: replace"),
return_value="note_contents", return_value="note_contents",
) as mock_write_in_editor: ) as mock_write_in_editor:
run(parser) run(parser)
@ -84,9 +84,9 @@ def test_override_configured_colors(
): ):
mock_load_or_install.return_value = minimal_config 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) 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( with mock.patch.object(
jrnl, jrnl,
"_write_in_editor", "_write_in_editor",

View file

@ -16,24 +16,24 @@ def minimal_config():
def test_apply_override(minimal_config): def test_apply_override(minimal_config):
config = minimal_config.copy() config = minimal_config.copy()
overrides = {"editor": "nano"} overrides = [{"editor": "nano"}]
config = apply_overrides(overrides, config) config = apply_overrides(overrides, config)
assert config["editor"] == "nano" assert config["editor"] == "nano"
def test_override_dot_notation(minimal_config): def test_override_dot_notation(minimal_config):
cfg = minimal_config.copy() cfg = minimal_config.copy()
overrides = {"colors.body": "blue"} overrides = [{"colors.body": "blue"}]
cfg = apply_overrides(overrides=overrides, base_config=cfg) cfg = apply_overrides(overrides=overrides, base_config=cfg)
assert cfg["colors"] == {"body": "blue", "date": "green"} assert cfg["colors"] == {"body": "blue", "date": "green"}
def test_multiple_overrides(minimal_config): def test_multiple_overrides(minimal_config):
overrides = { overrides = [
"colors.title": "magenta", {"colors.title": "magenta"},
"editor": "nano", {"editor": "nano"},
"journals.burner": "/tmp/journals/burner.jrnl", {"journals.burner": "/tmp/journals/burner.jrnl"},
} # as returned by parse_args, saved in parser.config_override ] # as returned by parse_args, saved in parser.config_override
cfg = apply_overrides(overrides, minimal_config.copy()) cfg = apply_overrides(overrides, minimal_config.copy())
assert cfg["editor"] == "nano" assert cfg["editor"] == "nano"

View file

@ -36,7 +36,7 @@ def expected_args(**kwargs):
"strict": False, "strict": False,
"tags": False, "tags": False,
"text": [], "text": [],
"config_override": None, "config_override": [],
} }
return {**default_args, **kwargs} return {**default_args, **kwargs}
@ -209,26 +209,26 @@ def test_version_alone():
def test_editor_override(): def test_editor_override():
parsed_args = cli_as_dict('--config-override editor:"nano"') parsed_args = cli_as_dict('--config-override editor "nano"')
assert parsed_args == expected_args(config_override={"editor": "nano"}) assert parsed_args == expected_args(config_override=[{"editor": "nano"}])
def test_color_override(): def test_color_override():
assert cli_as_dict("--config-override colors.body:blue") == expected_args( assert cli_as_dict("--config-override colors.body blue") == expected_args(
config_override={"colors.body": "blue"} config_override=[{"colors.body": "blue"}]
) )
def test_multiple_overrides(): def test_multiple_overrides():
parsed_args = cli_as_dict( 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( assert parsed_args == expected_args(
config_override={ config_override=[
"colors.title": "green", {"colors.title": "green"},
"journal.scratchpad": "/tmp/scratchpad", {"editor": "nano"},
"editor": "nano", {"journal.scratchpad": "/tmp/scratchpad"},
} ]
) )
@ -266,49 +266,27 @@ class TestDeserialization:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"input_str", "input_str",
[ [
'editor:"nano", colors.title:blue, default:"/tmp/egg.txt"', ["editor", '"nano"'],
'editor:"vi -c startinsert", colors.title:blue, default:"/tmp/egg.txt"', ["colors.title", "blue"],
'editor:"nano", colors.title:blue, default:"/tmp/eg\ g.txt"', ["default", "/tmp/egg.txt"],
], ],
) )
def test_deserialize_multiword_strings(self, input_str): def test_deserialize_multiword_strings(self, input_str):
runtime_config = deserialize_config_args(input_str) runtime_config = deserialize_config_args(input_str)
assert runtime_config.__class__ == dict assert runtime_config.__class__ == dict
assert "editor" in runtime_config.keys() assert input_str[0] in runtime_config.keys()
assert "colors.title" in runtime_config.keys() assert runtime_config[input_str[0]] == input_str[1]
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
def test_deserialize_multiple_datatypes(self): def test_deserialize_multiple_datatypes(self):
input = ( cfg = deserialize_config_args(["linewrap", "23"])
'linewrap: 23, encrypt: false, editor:"vi -c startinsert", highlight: true'
)
cfg = deserialize_config_args(input)
assert cfg["encrypt"] == False
assert cfg["highlight"] == True
assert cfg["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"' assert cfg["editor"] == '"vi -c startinsert"'
@pytest.mark.parametrize( cfg = deserialize_config_args(["highlight", "true"])
"delimiter", assert cfg["highlight"] == True
[
".",
":",
", ", # 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"]