diff --git a/.gitignore b/.gitignore index 5e11d85b..629a9504 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,5 @@ site/ .vscode/settings.json coverage.xml .vscode/launch.json -.coverage \ No newline at end of file +.coverage +.vscode/tasks.json \ No newline at end of file diff --git a/features/data/configs/tiny.yaml b/features/data/configs/tiny.yaml index e9da7943..578cd234 100644 --- a/features/data/configs/tiny.yaml +++ b/features/data/configs/tiny.yaml @@ -1,7 +1,8 @@ journals: - default: /tmp/journal.jrnl + default: features/journals/simple.journal colors: body: none title: green editor: "vim" encrypt: false +# \ No newline at end of file diff --git a/features/overrides.feature b/features/overrides.feature index 0aa84d8c..eef89eec 100644 --- a/features/overrides.feature +++ b/features/overrides.feature @@ -1,45 +1,52 @@ Feature: Implementing Runtime Overrides for Select Configuration Keys -Scenario: Override configured editor with built-in input === editor:'' -Given we use the config "editor-args.yaml" -When we run "jrnl --config-override '{"editor": ""}'" -Then the editor "" should have been called + Scenario: Override configured editor with built-in input === editor:'' + Given we use the config "tiny.yaml" + When we run jrnl with --config-override editor:'' + Then the stdin prompt must be launched -Scenario: Override configured editor with 'nano' -Given we use the config "editor.yaml" -When we run "jrnl --config-override '{"editor": "nano"}'" -Then the editor "nano" should have been called + @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" + Then the output should be -@skip_win -Scenario: Override configured linewrap with a value of 23 -Given we use the config "editor.yaml" -When we run "jrnl -2 --config-override '{"linewrap": 23}' --format fancy" -Then the output should be -""" -┎─────╮2013-06-09 15:39 -┃ My ╘═══════════════╕ -┃ fir st ent ry. │ -┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -┃ Everything is │ -┃ alright │ -┖─────────────────────┘ -┎─────╮2013-06-10 15:40 -┃ Lif ╘═══════════════╕ -┃ e is goo d. │ -┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ -┃ But I'm better. │ -┖─────────────────────┘ -""" + """ + ┎─────╮2013-06-09 15:39 + ┃ My ╘═══════════════╕ + ┃ fir st ent ry. │ + ┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + ┃ Everything is │ + ┃ alright │ + ┖─────────────────────┘ + ┎─────╮2013-06-10 15:40 + ┃ Lif ╘═══════════════╕ + ┃ e is goo d. │ + ┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤ + ┃ But I'm better. │ + ┖─────────────────────┘ + """ -@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"}' -Then the runtime config should have colors.body set to blue + @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 + 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"}' -Then the runtime config should have colors.body set to green -And the runtime config should have editor set to nano + @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" + Then the runtime config should have colors.body set to green + 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:"" + Then the editor should have been called + Examples: Editor Commands + | editor | + | nano | + | vi -c startinsert | + | code -w - | diff --git a/features/steps/override.py b/features/steps/override.py index 6ecbc8dc..ac531235 100644 --- a/features/steps/override.py +++ b/features/steps/override.py @@ -10,7 +10,18 @@ import yaml from yaml.loader import FullLoader 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}") @@ -31,30 +42,27 @@ def run_command(context, args): @then("the runtime config should have {key_as_dots} set to {override_value}") def config_override(context, key_as_dots: str, override_value: str): 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): print("callback executed") # fmt: off try: with \ + mock.patch("jrnl.jrnl.search_mode"), \ 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.time.parse', side_effect=_mock_time_parse(context)), \ 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) \ : run(context.parser) + runtime_cfg = mock_recurse.call_args_list[0][0][0] - assert mock_recurse.call_count >= 2 - mock_recurse.call_args_list = expected_call_args_list - + for k in key_as_vec: + runtime_cfg = runtime_cfg['%s'%k] + + assert runtime_cfg == override_value except SystemExit as e : context.exit_status = e.code # 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") def editor_override(context, editor): - def _mock_editor(command_and_journal_file): - editor = command_and_journal_file[0] - tmpfile = command_and_journal_file[-1] - context.tmpfile = tmpfile + def _mock_write_in_editor(config): + editor = config["editor"] + journal = "features/journals/journal.jrnl" + context.tmpfile = journal print("%s has been launched" % editor) - return tmpfile + return journal # fmt: off # see: https://github.com/psf/black/issues/664 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("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.install.get_config_path", side_effect=lambda: context.config_path) \ : try : - cli(['--config-override','{"editor": "%s"}'%editor]) + run(context.parser) context.exit_status = 0 - context.editor = mock_editor - assert mock_editor.assert_called_once_with(editor, context.tmpfile) + context.editor = mock_write_in_editor + 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: context.exit_status = e.code # 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 diff --git a/jrnl/args.py b/jrnl/args.py index 99489b74..f2ea288c 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -4,7 +4,6 @@ import argparse import re import textwrap -import json from .commands import postconfig_decrypt from .commands import postconfig_encrypt @@ -18,6 +17,22 @@ from .plugins import IMPORT_FORMATS 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): """Used in help screen""" @@ -323,18 +338,18 @@ def parse_args(args=[]): "--config-override", dest="config_override", action="store", - type=json.loads, + type=deserialize_config_args, nargs="?", - default={}, + default=None, metavar="CONFIG_KV_PAIR", help=""" Override configured key-value pairs 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 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, colors.title: green """, ) @@ -342,4 +357,5 @@ def parse_args(args=[]): num = re.compile(r"^-(\d+)$") 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 diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index fdc8c87b..25fd191a 100644 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -16,6 +16,7 @@ from .editor import get_text_from_editor from .editor import get_text_from_stdin from .exception import UserAbort from . import time +from .override import apply_overrides def run(args): @@ -51,9 +52,8 @@ def run(args): # Apply config overrides overrides = args.config_override - from .override import apply_overrides - - config = apply_overrides(overrides, config) + if overrides: + config = apply_overrides(overrides, config) # --- All the standalone commands are now done --- # diff --git a/tests/test_config.py b/tests/test_config.py index 20e1236a..dc2650a8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,7 +1,7 @@ +import shlex import pytest import mock -import yaml from jrnl.args import parse_args from jrnl.jrnl import run @@ -43,7 +43,7 @@ def test_override_configured_editor( mock_load_or_install.return_value = minimal_config mock_isatty.return_value = True - cli_args = ["--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() @@ -84,7 +84,7 @@ def test_override_configured_colors( ): 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) assert "colors.body" in parser.config_override.keys() with mock.patch.object( diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index 6ce40338..a49c99cb 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -1,9 +1,9 @@ -from features.steps.override import config_override import shlex import pytest from jrnl.args import parse_args +from jrnl.args import deserialize_config_args def cli_as_dict(str): @@ -36,7 +36,7 @@ def expected_args(**kwargs): "strict": False, "tags": False, "text": [], - "config_override": {}, + "config_override": None, } return {**default_args, **kwargs} @@ -207,27 +207,58 @@ def test_version_alone(): 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(): - assert cli_as_dict('--config-override \'{"editor": "nano"}\'') == 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(): - assert cli_as_dict( - '--config-override \'{"colors.title": "green", "editor":"", "journal.scratchpad": "/tmp/scratchpad"}\'' - ) == expected_args( + parsed_args = cli_as_dict( + '--config-override colors.title:green,editor:"nano",journal.scratchpad:"/tmp/scratchpad"' + ) + assert parsed_args == expected_args( config_override={ "colors.title": "green", "journal.scratchpad": "/tmp/scratchpad", - "editor": "", + "editor": "nano", } )