mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-20 04:58:32 +02:00
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:
parent
40f93a9322
commit
6e658c31df
6 changed files with 82 additions and 114 deletions
|
@ -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:"<editor>"
|
||||
When we run jrnl with --config-override editor "<editor>"
|
||||
Then the editor <editor> should have been called
|
||||
Examples: Editor Commands
|
||||
| editor |
|
||||
|
|
55
jrnl/args.py
55
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,14 +26,11 @@ 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)
|
||||
|
||||
l = input[0]
|
||||
r = input[1]
|
||||
r = r.strip()
|
||||
if r.isdigit():
|
||||
r = int(r)
|
||||
|
@ -57,10 +42,14 @@ def deserialize_config_args(input: str) -> dict:
|
|||
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
|
||||
""",
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue