Merge pull request #2 from sriniv27/double-delimited-text-input-for-config-override

Double delimited text input for config override
This commit is contained in:
Suhas 2021-01-28 08:51:30 -05:00 committed by GitHub
commit 18458b7299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 178 additions and 87 deletions

1
.gitignore vendored
View file

@ -59,3 +59,4 @@ site/
coverage.xml coverage.xml
.vscode/launch.json .vscode/launch.json
.coverage .coverage
.vscode/tasks.json

View file

@ -1,7 +1,8 @@
journals: journals:
default: /tmp/journal.jrnl default: features/journals/simple.journal
colors: colors:
body: none body: none
title: green title: green
editor: "vim" editor: "vim"
encrypt: false encrypt: false
#

View file

@ -1,45 +1,52 @@
Feature: Implementing Runtime Overrides for Select Configuration Keys 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 "editor-args.yaml" Given we use the config "tiny.yaml"
When we run "jrnl --config-override '{"editor": ""}'" When we run jrnl with --config-override editor:''
Then the editor "" should have been called Then the stdin prompt must be launched
Scenario: Override configured editor with 'nano' @skip_win
Given we use the config "editor.yaml" Scenario: Override configured linewrap with a value of 23
When we run "jrnl --config-override '{"editor": "nano"}'" Given we use the config "tiny.yaml"
Then the editor "nano" should have been called When we run "jrnl -2 --config-override linewrap:23 --format fancy"
Then the output should be
@skip_win """
Scenario: Override configured linewrap with a value of 23 2013-06-09 15:39
Given we use the config "editor.yaml" My
When we run "jrnl -2 --config-override '{"linewrap": 23}' --format fancy" fir st ent ry.
Then the output should be
""" Everything is
2013-06-09 15:39 alright
My
fir st ent ry. 2013-06-10 15:40
Lif
Everything is e is goo d.
alright
But I'm better.
2013-06-10 15:40
Lif """
e is goo d.
But I'm better.
"""
@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,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
Scenario Outline: Override configured editor
Given we use the config "tiny.yaml"
When we run jrnl with --config-override editor:"<editor>"
Then the editor <editor> should have been called
Examples: Editor Commands
| editor |
| nano |
| vi -c startinsert |
| code -w - |

View file

@ -10,7 +10,18 @@ import yaml
from yaml.loader import FullLoader from yaml.loader import FullLoader
import jrnl import jrnl
from jrnl.cli import cli
def _mock_time_parse(context):
original_parse = jrnl.time.parse
if "now" not in context:
return original_parse
def wrapper(input, *args, **kwargs):
input = context.now if input == "now" else input
return original_parse(input, *args, **kwargs)
return wrapper
@given("we use the config {config_file}") @given("we use the config {config_file}")
@ -31,13 +42,6 @@ def run_command(context, args):
@then("the runtime config should have {key_as_dots} set to {override_value}") @then("the runtime config should have {key_as_dots} set to {override_value}")
def config_override(context, key_as_dots: str, override_value: str): def config_override(context, key_as_dots: str, override_value: str):
key_as_vec = key_as_dots.split(".") key_as_vec = key_as_dots.split(".")
expected_call_args_list = [
(context.cfg, key_as_vec, override_value),
(context.cfg[key_as_vec[0]], key_as_vec[1], override_value),
]
with open(context.config_path) as f:
loaded_cfg = yaml.load(f, Loader=yaml.FullLoader)
loaded_cfg["journal"] = "features/journals/simple.journal"
def _mock_callback(**args): def _mock_callback(**args):
print("callback executed") print("callback executed")
@ -45,16 +49,20 @@ def config_override(context, key_as_dots: str, override_value: str):
# fmt: off # fmt: off
try: try:
with \ with \
mock.patch("jrnl.jrnl.search_mode"), \
mock.patch.object(jrnl.override,"_recursively_apply",wraps=jrnl.override._recursively_apply) as mock_recurse, \ mock.patch.object(jrnl.override,"_recursively_apply",wraps=jrnl.override._recursively_apply) as mock_recurse, \
mock.patch('jrnl.install.load_or_install_jrnl', return_value=context.cfg), \ mock.patch('jrnl.install.load_or_install_jrnl', return_value=context.cfg), \
mock.patch('jrnl.time.parse', side_effect=_mock_time_parse(context)), \
mock.patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \ mock.patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
mock.patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \ mock.patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
: :
run(context.parser) run(context.parser)
runtime_cfg = mock_recurse.call_args_list[0][0][0]
assert mock_recurse.call_count >= 2 for k in key_as_vec:
mock_recurse.call_args_list = expected_call_args_list runtime_cfg = runtime_cfg['%s'%k]
assert runtime_cfg == override_value
except SystemExit as e : except SystemExit as e :
context.exit_status = e.code context.exit_status = e.code
# fmt: on # fmt: on
@ -62,27 +70,54 @@ def config_override(context, key_as_dots: str, override_value: str):
@then("the editor {editor} should have been called") @then("the editor {editor} should have been called")
def editor_override(context, editor): def editor_override(context, editor):
def _mock_editor(command_and_journal_file): def _mock_write_in_editor(config):
editor = command_and_journal_file[0] editor = config["editor"]
tmpfile = command_and_journal_file[-1] journal = "features/journals/journal.jrnl"
context.tmpfile = tmpfile context.tmpfile = journal
print("%s has been launched" % editor) print("%s has been launched" % editor)
return tmpfile return journal
# fmt: off # fmt: off
# see: https://github.com/psf/black/issues/664 # see: https://github.com/psf/black/issues/664
with \ with \
mock.patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \ mock.patch("jrnl.jrnl._write_in_editor", side_effect=_mock_write_in_editor(context.cfg)) as mock_write_in_editor, \
mock.patch("sys.stdin.isatty", return_value=True), \ mock.patch("sys.stdin.isatty", return_value=True), \
mock.patch("jrnl.time.parse"), \ mock.patch("jrnl.time.parse", side_effect = _mock_time_parse(context)), \
mock.patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \ mock.patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
mock.patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \ mock.patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
: :
try : try :
cli(['--config-override','{"editor": "%s"}'%editor]) run(context.parser)
context.exit_status = 0 context.exit_status = 0
context.editor = mock_editor context.editor = mock_write_in_editor
assert mock_editor.assert_called_once_with(editor, context.tmpfile) expected_config = context.cfg
expected_config['editor'] = '%s'%editor
expected_config['journal'] ='features/journals/journal.jrnl'
assert mock_write_in_editor.call_count == 1
assert mock_write_in_editor.call_args[0][0]['editor']==editor
except SystemExit as e: except SystemExit as e:
context.exit_status = e.code context.exit_status = e.code
# fmt: on # fmt: on
@then("the stdin prompt must be launched")
def override_editor_to_use_stdin(context):
try:
with mock.patch(
"sys.stdin.read",
return_value="Zwei peanuts walk into a bar und one of zem was a-salted",
) as mock_stdin_read, mock.patch(
"jrnl.install.load_or_install_jrnl", return_value=context.cfg
), mock.patch(
"jrnl.Journal.open_journal",
spec=False,
return_value="features/journals/journal.jrnl",
):
run(context.parser)
context.exit_status = 0
mock_stdin_read.assert_called_once()
except SystemExit as e:
context.exit_status = e.code

View file

@ -4,7 +4,6 @@
import argparse import argparse
import re import re
import textwrap import textwrap
import json
from .commands import postconfig_decrypt from .commands import postconfig_decrypt
from .commands import postconfig_encrypt from .commands import postconfig_encrypt
@ -18,6 +17,22 @@ from .plugins import IMPORT_FORMATS
from .plugins import util from .plugins import util
def deserialize_config_args(input: str) -> dict:
_kvpairs = input.strip(" ").split(",")
runtime_modifications = {}
for _p in _kvpairs:
l, r = _p.strip().split(":")
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
class WrappingFormatter(argparse.RawTextHelpFormatter): class WrappingFormatter(argparse.RawTextHelpFormatter):
"""Used in help screen""" """Used in help screen"""
@ -323,18 +338,18 @@ def parse_args(args=[]):
"--config-override", "--config-override",
dest="config_override", dest="config_override",
action="store", action="store",
type=json.loads, type=deserialize_config_args,
nargs="?", nargs="?",
default={}, default=None,
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 pairs 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, colors.title: green
""", """,
) )
@ -342,4 +357,5 @@ def parse_args(args=[]):
num = re.compile(r"^-(\d+)$") num = re.compile(r"^-(\d+)$")
args = [num.sub(r"-n \1", arg) for arg in args] args = [num.sub(r"-n \1", arg) for arg in args]
return parser.parse_intermixed_args(args) parsed_args = parser.parse_intermixed_args(args)
return parsed_args

View file

@ -16,6 +16,7 @@ from .editor import get_text_from_editor
from .editor import get_text_from_stdin from .editor import get_text_from_stdin
from .exception import UserAbort from .exception import UserAbort
from . import time from . import time
from .override import apply_overrides
def run(args): def run(args):
@ -51,9 +52,8 @@ def run(args):
# Apply config overrides # Apply config overrides
overrides = args.config_override overrides = args.config_override
from .override import apply_overrides if overrides:
config = apply_overrides(overrides, config)
config = apply_overrides(overrides, config)
# --- All the standalone commands are now done --- # # --- All the standalone commands are now done --- #

View file

@ -1,7 +1,7 @@
import shlex
import pytest import pytest
import mock import mock
import yaml
from jrnl.args import parse_args from jrnl.args import parse_args
from jrnl.jrnl import run from jrnl.jrnl import run
@ -43,7 +43,7 @@ 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 = ["--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" in parser.config_override.keys()
@ -84,7 +84,7 @@ def test_override_configured_colors(
): ):
mock_load_or_install.return_value = minimal_config mock_load_or_install.return_value = minimal_config
cli_args = ["--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" in parser.config_override.keys()
with mock.patch.object( with mock.patch.object(

View file

@ -1,9 +1,9 @@
from features.steps.override import config_override
import shlex import shlex
import pytest import pytest
from jrnl.args import parse_args from jrnl.args import parse_args
from jrnl.args import deserialize_config_args
def cli_as_dict(str): def cli_as_dict(str):
@ -36,7 +36,7 @@ def expected_args(**kwargs):
"strict": False, "strict": False,
"tags": False, "tags": False,
"text": [], "text": [],
"config_override": {}, "config_override": None,
} }
return {**default_args, **kwargs} return {**default_args, **kwargs}
@ -207,27 +207,58 @@ def test_version_alone():
assert cli_as_dict("--version") == expected_args(preconfig_cmd=preconfig_version) assert cli_as_dict("--version") == expected_args(preconfig_cmd=preconfig_version)
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"',
],
)
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
def test_deserialize_multiple_datatypes(self):
input = 'linewrap: 23, encrypt: false, editor:"vi -c startinsert"'
cfg = deserialize_config_args(input)
assert cfg["encrypt"] == False
assert cfg["linewrap"] == 23
assert cfg["editor"] == '"vi -c startinsert"'
def test_editor_override(): def test_editor_override():
assert cli_as_dict('--config-override \'{"editor": "nano"}\'') == expected_args( parsed_args = cli_as_dict('--config-override editor:"nano"')
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( assert cli_as_dict("--config-override colors.body:blue") == expected_args(
'--config-override \'{"colors.body": "blue"}\'' config_override={"colors.body": "blue"}
) == expected_args(config_override={"colors.body": "blue"}) )
def test_multiple_overrides(): def test_multiple_overrides():
assert cli_as_dict( parsed_args = cli_as_dict(
'--config-override \'{"colors.title": "green", "editor":"", "journal.scratchpad": "/tmp/scratchpad"}\'' '--config-override colors.title:green,editor:"nano",journal.scratchpad:"/tmp/scratchpad"'
) == expected_args( )
assert parsed_args == expected_args(
config_override={ config_override={
"colors.title": "green", "colors.title": "green",
"journal.scratchpad": "/tmp/scratchpad", "journal.scratchpad": "/tmp/scratchpad",
"editor": "", "editor": "nano",
} }
) )