diff --git a/.gitignore b/.gitignore index afb0d8743..374deb4bb 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,10 @@ exp/ _extras/ *.sublime-* site/ + +.vscode/settings.json +coverage.xml +.vscode/launch.json +.coverage +.vscode/tasks.json +todo.txt diff --git a/docs/advanced.md b/docs/advanced.md index 51c4d1afa..4972cf13b 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -61,6 +61,29 @@ and can be edited with a plain text editor. Or use the built-in prompt or an external editor to compose your entries. +### Modifying Configurations from the Command line + +You can override a configuration field for the current instance of `jrnl` using `--config-override CONFIG_KEY CONFIG_VALUE` where `CONFIG_KEY` is a valid configuration field, specified in dot-notation and `CONFIG_VALUE` is the (valid) desired override value. + +You can specify multiple overrides as multiple calls to `--config-override`. +!!! note + These overrides allow you to modify ***any*** field of your jrnl configuration. We trust that you know what you are doing. + +#### Examples: + +``` sh +#Create an entry using the `stdin` prompt, for rapid logging +jrnl --config-override editor "" + +#Populate a project's log +jrnl --config-override journals.todo "$(git rev-parse --show-toplevel)/todo.txt" todo find my towel + +#Pass multiple overrides +jrnl --config-override display_format fancy --config-override linewrap 20 \ +--config-override colors.title green + +``` + ## Multiple journal files You can configure `jrnl`to use with multiple journals (eg. diff --git a/docs/recipes.md b/docs/recipes.md index 14e08e14e..ef45666a3 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -154,6 +154,33 @@ only field 1. jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)" ``` + +### Launch a terminal for rapid logging +You can use this to launch a terminal that is the `jrnl` stdin prompt so you can start typing away immediately. + +```bash +jrnl now --config-override editor:"" +``` + +Bind this to a keyboard shortcut. + +Map `Super+Alt+J` to launch the terminal with jrnl prompt + +- **xbindkeys** +In your `.xbindkeysrc` + +```ini +Mod4+Mod1+j + alacritty -t floating-jrnl -e jrnl now --config-override editor:"", +``` + +- **I3 WM** Launch a floating terminal with the `jrnl` prompt + +```ini +bindsym Mod4+Mod1+j exec --no-startup-id alacritty -t floating-jrnl -e jrnl --config-override editor:"" +for_window[title="floating *"] floating enable +``` + ## External editors Configure your preferred external editor by updating the `editor` option diff --git a/features/overrides.feature b/features/overrides.feature new file mode 100644 index 000000000..e0cdd9f0a --- /dev/null +++ b/features/overrides.feature @@ -0,0 +1,98 @@ +Feature: Implementing Runtime Overrides for Select Configuration Keys + + Scenario: Override configured editor with built-in input === editor:'' + Given we use the config "basic_encrypted.yaml" + And we use the password "test" if prompted + When we run "jrnl --config-override editor ''" + Then the stdin prompt should have been called + + Scenario: Postconfig commands with overrides + Given We use the config "basic_encrypted.yaml" + And we use the password "test" if prompted + When we run "jrnl --decrypt --config-override highlight false --config-override editor nano" + Then the config should have "highlight" set to "bool:false" + And no editor should have been called + + Scenario: Override configured linewrap with a value of 23 + Given we use the config "simple.yaml" + And we use the password "test" if prompted + 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. │ + ┖─────────────────────┘ + """ + + Scenario: Override color selections with runtime overrides + Given we use the config "basic_encrypted.yaml" + And we use the password "test" if prompted + When we run "jrnl -1 --config-override colors.body blue" + Then the config should have "colors.body" set to "blue" + + Scenario: Apply multiple config overrides + Given we use the config "basic_encrypted.yaml" + And we use the password "test" if prompted + When we run "jrnl -1 --config-override colors.body green --config-override editor 'nano'" + Then the config should have "colors.body" set to "green" + And the config should have "editor" set to "nano" + + + Scenario Outline: Override configured editor + Given we use the config "basic_encrypted.yaml" + And we use the password "test" if prompted + When we run "jrnl --config-override editor ''" + Then the editor should have been called + Examples: Editor Commands + | editor | + | nano | + | vi -c startinsert | + | code -w | + + Scenario: Override default journal + Given we use the config "basic_dayone.yaml" + And we use the password "test" if prompted + When we run "jrnl --debug --config-override journals.default features/journals/simple.journal 20 Mar 2000: The rain in Spain comes from clouds" + Then we should get no error + And we should see the message "Entry added" + When we run "jrnl -3 --debug --config-override journals.default features/journals/simple.journal" + Then the output should be + """ + 2000-03-20 09:00 The rain in Spain comes from clouds + + 2013-06-09 15:39 My first entry. + | Everything is alright + + 2013-06-10 15:40 Life is good. + | But I'm better. + """ + + + Scenario: Make an entry into an overridden journal + Given we use the config "basic_dayone.yaml" + And we use the password "test" if prompted + When we run "jrnl --config-override journals.temp features/journals/simple.journal temp Sep 06 1969: @say Ni" + Then we should get no error + And we should see the message "Entry added" + When we run "jrnl --config-override journals.temp features/journals/simple.journal temp -3" + Then the output should be + """ + 1969-09-06 09:00 @say Ni + + 2013-06-09 15:39 My first entry. + | Everything is alright + + 2013-06-10 15:40 Life is good. + | But I'm better. + """ diff --git a/features/steps/core.py b/features/steps/core.py index abac49176..f471acfba 100644 --- a/features/steps/core.py +++ b/features/steps/core.py @@ -3,6 +3,7 @@ import ast from collections import defaultdict +from jrnl.args import parse_args import os from pathlib import Path import re @@ -13,8 +14,11 @@ from behave import then from behave import when import keyring + import toml import yaml +from yaml.loader import FullLoader + import jrnl.time from jrnl import Journal @@ -23,6 +27,7 @@ from jrnl.cli import cli from jrnl.config import load_config from jrnl.os_compat import split_args +from jrnl.override import apply_overrides, _recursively_apply try: import parsedatetime.parsedatetime_consts as pdt @@ -114,8 +119,15 @@ def read_value_from_string(string): return ast.literal_eval(string) # Takes strings like "bool:true" or "int:32" and coerces them into proper type - t, value = string.split(":") - value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value) + string_parts = string.split(":") + if len(string_parts) > 1: + type = string_parts[0] + value = string_parts[1:][0] # rest of the text + value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[type]( + value + ) + else: + value = string_parts[0] return value @@ -315,6 +327,7 @@ def run_with_input(context, command, inputs=""): text = iter([inputs]) args = split_args(command)[1:] + context.args = args def _mock_editor(command): context.editor_command = command @@ -397,8 +410,13 @@ def run(context, command, text=""): if "cache_dir" in context and context.cache_dir is not None: cache_dir = os.path.join("features", "cache", context.cache_dir) command = command.format(cache_dir=cache_dir) + if "config_path" in context and context.config_path is not None: + with open(context.config_path, "r") as f: + cfg = yaml.load(f, Loader=FullLoader) + context.jrnl_config = cfg args = split_args(command) + context.args = args[1:] def _mock_editor(command): context.editor_command = command @@ -604,14 +622,29 @@ def journal_exists(context, journal_name="default"): @then('the config should have "{key}" set to "{value}"') @then('the config for journal "{journal}" should have "{key}" set to "{value}"') def config_var(context, key, value="", journal=None): + key_as_vec = key.split(".") + + if "args" in context: + parsed = parse_args(context.args) + overrides = parsed.config_override value = read_value_from_string(value or context.text or "") configuration = load_config(context.config_path) if journal: configuration = configuration["journals"][journal] - assert key in configuration - assert configuration[key] == value + if overrides: + with patch.object( + jrnl.override, "_recursively_apply", wraps=_recursively_apply + ) as spy_recurse: + configuration = apply_overrides(overrides, configuration) + runtime_cfg = spy_recurse.call_args_list[0][0][0] + else: + runtime_cfg = configuration + # extract the value of the desired key from the configuration after overrides have been applied + for k in key_as_vec: + runtime_cfg = runtime_cfg["%s" % k] + assert runtime_cfg == value @then('the config for journal "{journal}" should not have "{key}" set') diff --git a/features/steps/override.py b/features/steps/override.py new file mode 100644 index 000000000..ff1760ed6 --- /dev/null +++ b/features/steps/override.py @@ -0,0 +1,77 @@ +from jrnl.jrnl import run +from unittest import mock + +# from __future__ import with_statement +from jrnl.args import parse_args +from behave import then + +from features.steps.core import _mock_getpass, _mock_time_parse + + +@then("the editor {editor} should have been called") +@then("No editor should have been called") +def editor_override(context, editor=None): + def _mock_write_in_editor(config): + editor = config["editor"] + journal = "features/journals/journal.jrnl" + context.tmpfile = journal + print("%s has been launched" % editor) + return journal + + if "password" in context: + password = context.password + else: + password = "" + # fmt: off + # see: https://github.com/psf/black/issues/664 + with \ + mock.patch("jrnl.jrnl._write_in_editor", side_effect=_mock_write_in_editor(context.jrnl_config)) as mock_write_in_editor, \ + mock.patch("sys.stdin.isatty", return_value=True), \ + mock.patch('getpass.getpass',side_effect=_mock_getpass(password)), \ + 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 : + parsed_args = parse_args(context.args) + run(parsed_args) + context.exit_status = 0 + context.editor = mock_write_in_editor + expected_config = context.jrnl_config + expected_config['editor'] = '%s'%editor + expected_config['journal'] ='features/journals/journal.jrnl' + + if editor is not None: + assert mock_write_in_editor.call_count == 1 + assert mock_write_in_editor.call_args[0][0]['editor']==editor + else: + # Expect that editor is *never* called + mock_write_in_editor.assert_not_called() + except SystemExit as e: + context.exit_status = e.code + # fmt: on + + +@then("the stdin prompt should have been called") +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.jrnl_config + ), mock.patch( + "jrnl.Journal.open_journal", + spec=False, + return_value="features/journals/journal.jrnl", + ), mock.patch( + "getpass.getpass", side_effect=_mock_getpass("test") + ): + parsed_args = parse_args(context.args) + run(parsed_args) + 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 f934ca160..c8bd77438 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -314,6 +314,29 @@ def parse_args(args=[]): help=argparse.SUPPRESS, ) + config_overrides = parser.add_argument_group( + "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="append", + type=str, + nargs=2, + default=[], + metavar="CONFIG_KV_PAIR", + help=""" + 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 --config-override colors.title green + """, + ) + # Handle '-123' as a shortcut for '-n 123' num = re.compile(r"^-(\d+)$") args = [num.sub(r"-n \1", arg) for arg in args] diff --git a/jrnl/config.py b/jrnl/config.py index a5a1d1cc1..da2df2cce 100644 --- a/jrnl/config.py +++ b/jrnl/config.py @@ -19,6 +19,32 @@ DEFAULT_JOURNAL_NAME = "journal.txt" DEFAULT_JOURNAL_KEY = "default" +YAML_SEPARATOR = ": " + + +def make_yaml_valid_dict(input: list) -> dict: + + """ + + Convert a two-element list of configuration key-value pair into a flat dict. + + The dict is created through the yaml loader, with the assumption that + "input[0]: input[1]" is valid yaml. + + :param input: list of configuration keys in dot-notation and their respective values. + :type input: list + :return: A single level dict of the configuration keys in dot-notation and their respective desired values + :rtype: dict + """ + + assert len(input) == 2 + + # yaml compatible strings are of the form Key:Value + yamlstr = YAML_SEPARATOR.join(input) + runtime_modifications = yaml.load(yamlstr, Loader=yaml.FullLoader) + + return runtime_modifications + def save_config(config): config["version"] = __version__ diff --git a/jrnl/jrnl.py b/jrnl/jrnl.py index 257358c45..383cceeed 100644 --- a/jrnl/jrnl.py +++ b/jrnl/jrnl.py @@ -16,6 +16,7 @@ from .editor import get_text_from_stdin from .exception import UserAbort from . import time +from .override import apply_overrides def run(args): @@ -37,6 +38,12 @@ def run(args): try: config = install.load_or_install_jrnl() original_config = config.copy() + + # Apply config overrides + overrides = args.config_override + if overrides: + config = apply_overrides(overrides, config) + args = get_journal_name(args, config) config = scope_config(config, args.journal_name) except UserAbort as err: diff --git a/jrnl/override.py b/jrnl/override.py new file mode 100644 index 000000000..7fd718f03 --- /dev/null +++ b/jrnl/override.py @@ -0,0 +1,65 @@ +from .config import update_config, make_yaml_valid_dict + +# import logging +def apply_overrides(overrides: list, base_config: dict) -> dict: + """Unpack CLI provided overrides into the configuration tree. + + :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 + """ + cfg_with_overrides = base_config.copy() + for pairs in overrides: + + pairs = make_yaml_valid_dict(pairs) + key_as_dots, override_value = _get_key_and_value_from_pair(pairs) + keys = _convert_dots_to_list(key_as_dots) + cfg_with_overrides = _recursively_apply( + cfg_with_overrides, keys, override_value + ) + + update_config(base_config, cfg_with_overrides, None) + return base_config + + +def _get_key_and_value_from_pair(pairs): + key_as_dots, override_value = list(pairs.items())[0] + return key_as_dots, override_value + + +def _convert_dots_to_list(key_as_dots): + keys = key_as_dots.split(".") + keys = [k for k in keys if k != ""] # remove empty elements + return keys + + +def _recursively_apply(tree: dict, nodes: list, override_value) -> dict: + """Recurse through configuration and apply overrides at the leaf of the config tree + + Credit to iJames on SO: https://stackoverflow.com/a/47276490 for algorithm + + Args: + config (dict): Configuration to modify + nodes (list): Vector of override keys; the length of the vector indicates tree depth + override_value (str): Runtime override passed from the command-line + """ + key = nodes[0] + if len(nodes) == 1: + tree[key] = override_value + else: + next_key = nodes[1:] + next_node = _get_config_node(tree, key) + _recursively_apply(next_node, next_key, override_value) + + return tree + + +def _get_config_node(config: dict, key: str): + if key in config: + pass + else: + config[key] = None + return config[key] diff --git a/poetry.lock b/poetry.lock index 50135423d..6418862f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -33,7 +33,7 @@ test = ["coverage", "flake8", "pexpect", "wheel"] [[package]] name = "asteval" -version = "0.9.21" +version = "0.9.22" description = "Safe, minimalistic evaluator of python expression using ast module" category = "main" optional = false @@ -129,22 +129,22 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cryptography" -version = "3.3.1" +version = "3.4.6" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" +python-versions = ">=3.6" [package.dependencies] cffi = ">=1.12" -six = ">=1.4.1" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "future" @@ -457,7 +457,7 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2020.5" +version = "2021.1" description = "World timezone definitions, modern and historical" category = "main" optional = false @@ -587,7 +587,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "yq" -version = "2.11.1" +version = "2.12.0" description = "Command-line YAML/XML processor - jq wrapper for YAML/XML documents" category = "dev" optional = false @@ -596,6 +596,7 @@ python-versions = "*" [package.dependencies] argcomplete = ">=1.8.1" PyYAML = ">=3.11" +toml = ">=0.10.0" xmltodict = ">=0.11.0" [package.extras] @@ -632,7 +633,7 @@ argcomplete = [ {file = "argcomplete-1.12.2.tar.gz", hash = "sha256:de0e1282330940d52ea92a80fea2e4b9e0da1932aaa570f84d268939d1897b04"}, ] asteval = [ - {file = "asteval-0.9.21.tar.gz", hash = "sha256:ee14ba2211cda1c76114e3e7b552cdd57e940309203d5f4106e6d6f2c2346a2e"}, + {file = "asteval-0.9.22.tar.gz", hash = "sha256:74a0939765fc6b1421e6672ccf74c52edc3c7af7d6a8298b057b0d50ac51aea8"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -683,6 +684,7 @@ cffi = [ {file = "cffi-1.14.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:840793c68105fe031f34d6a086eaea153a0cd5c491cde82a74b420edd0a2b909"}, {file = "cffi-1.14.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:b18e0a9ef57d2b41f5c68beefa32317d286c3d6ac0484efd10d6e07491bb95dd"}, {file = "cffi-1.14.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:045d792900a75e8b1e1b0ab6787dd733a8190ffcf80e8c8ceb2fb10a29ff238a"}, + {file = "cffi-1.14.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:7ef7d4ced6b325e92eb4d3502946c78c5367bc416398d387b39591532536734e"}, {file = "cffi-1.14.4-cp39-cp39-win32.whl", hash = "sha256:ba4e9e0ae13fc41c6b23299545e5ef73055213e466bd107953e4a013a5ddd7e3"}, {file = "cffi-1.14.4-cp39-cp39-win_amd64.whl", hash = "sha256:f032b34669220030f905152045dfa27741ce1a6db3324a5bc0b96b6c7420c87b"}, {file = "cffi-1.14.4.tar.gz", hash = "sha256:1a465cbe98a7fd391d47dce4b8f7e5b921e6cd805ef421d04f5f66ba8f06086c"}, @@ -696,20 +698,13 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] cryptography = [ - {file = "cryptography-3.3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030"}, - {file = "cryptography-3.3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0"}, - {file = "cryptography-3.3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812"}, - {file = "cryptography-3.3.1-cp27-cp27m-win32.whl", hash = "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e"}, - {file = "cryptography-3.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901"}, - {file = "cryptography-3.3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d"}, - {file = "cryptography-3.3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5"}, - {file = "cryptography-3.3.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c"}, - {file = "cryptography-3.3.1-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c"}, - {file = "cryptography-3.3.1-cp36-abi3-win32.whl", hash = "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a"}, - {file = "cryptography-3.3.1-cp36-abi3-win_amd64.whl", hash = "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7"}, - {file = "cryptography-3.3.1.tar.gz", hash = "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6"}, + {file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"}, + {file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"}, + {file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"}, + {file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"}, + {file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"}, ] future = [ {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, @@ -768,20 +763,39 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, + {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, + {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, + {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, + {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mkdocs = [ @@ -843,8 +857,8 @@ python-dateutil = [ {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] pytz = [ - {file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"}, - {file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"}, + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, @@ -1029,8 +1043,8 @@ xmltodict = [ {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, ] yq = [ - {file = "yq-2.11.1-py2.py3-none-any.whl", hash = "sha256:c1e1d6abb6e80beacb9c40f9eb1105b6fc2a08d3dc908237f69f197e683cff1b"}, - {file = "yq-2.11.1.tar.gz", hash = "sha256:74f64e3784a34d8a18efd8addc83cf5ca3478a0a69517d70fd9158a3809f99e0"}, + {file = "yq-2.12.0-py2.py3-none-any.whl", hash = "sha256:1f124f48dee77ad5e0be8607777fed183e96c8d31fa577de14201c7a614e4819"}, + {file = "yq-2.12.0.tar.gz", hash = "sha256:1d2ad403504d306b5258b86c698f9856d7ad58b7bb17a2b875691a6a7b8c4c20"}, ] zipp = [ {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, diff --git a/tests/test_override.py b/tests/test_override.py new file mode 100644 index 000000000..32ec0595c --- /dev/null +++ b/tests/test_override.py @@ -0,0 +1,79 @@ +import pytest + +from jrnl.override import ( + apply_overrides, + _recursively_apply, + _get_config_node, + _get_key_and_value_from_pair, + _convert_dots_to_list, +) + + +@pytest.fixture() +def minimal_config(): + cfg = { + "colors": {"body": "red", "date": "green"}, + "default": "/tmp/journal.jrnl", + "editor": "vim", + "journals": {"default": "/tmp/journals/journal.jrnl"}, + } + return cfg + + +def test_apply_override(minimal_config): + overrides = [["editor", "nano"]] + apply_overrides(overrides, minimal_config) + assert minimal_config["editor"] == "nano" + + +def test_override_dot_notation(minimal_config): + overrides = [["colors.body", "blue"]] + + cfg = apply_overrides(overrides=overrides, base_config=minimal_config) + 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 + + cfg = apply_overrides(overrides, minimal_config) + assert cfg["editor"] == "nano" + assert cfg["colors"]["title"] == "magenta" + assert "burner" in cfg["journals"] + assert cfg["journals"]["burner"] == "/tmp/journals/burner.jrnl" + + +def test_recursively_apply(): + cfg = {"colors": {"body": "red", "title": "green"}} + cfg = _recursively_apply(cfg, ["colors", "body"], "blue") + assert cfg["colors"]["body"] == "blue" + + +def test_get_config_node(minimal_config): + assert len(minimal_config.keys()) == 4 + assert _get_config_node(minimal_config, "editor") == "vim" + assert _get_config_node(minimal_config, "display_format") == None + + +def test_get_kv_from_pair(): + pair = {"ab.cde": "fgh"} + k, v = _get_key_and_value_from_pair(pair) + assert k == "ab.cde" + assert v == "fgh" + + +class TestDotNotationToList: + def test_unpack_dots_to_list(self): + + keys = "a.b.c.d.e.f" + keys_list = _convert_dots_to_list(keys) + assert len(keys_list) == 6 + + def test_sequential_delimiters(self): + k = "g.r..h.v" + k_l = _convert_dots_to_list(k) + assert len(k_l) == 4 diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index 252638c9e..4b140fc14 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -3,6 +3,7 @@ import pytest from jrnl.args import parse_args +from jrnl.config import make_yaml_valid_dict def cli_as_dict(str): @@ -35,6 +36,7 @@ def expected_args(**kwargs): "strict": False, "tags": False, "text": [], + "config_override": [], } return {**default_args, **kwargs} @@ -205,6 +207,31 @@ def test_version_alone(): assert cli_as_dict("--version") == expected_args(preconfig_cmd=preconfig_version) +def test_editor_override(): + + 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"]] + ) + + +def test_multiple_overrides(): + parsed_args = cli_as_dict( + '--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"], + ["editor", "nano"], + ["journal.scratchpad", "/tmp/scratchpad"], + ] + ) + + # @see https://github.com/jrnl-org/jrnl/issues/520 @pytest.mark.parametrize( "cli", @@ -233,3 +260,33 @@ def test_and_ordering(cli): def test_edit_ordering(cli): result = expected_args(edit=True, text=["second", "@oldtag", "@newtag"]) assert cli_as_dict(cli) == result + + +class TestDeserialization: + @pytest.mark.parametrize( + "input_str", + [ + ["editor", "nano"], + ["colors.title", "blue"], + ["default", "/tmp/egg.txt"], + ], + ) + def test_deserialize_multiword_strings(self, input_str): + + runtime_config = make_yaml_valid_dict(input_str) + assert runtime_config.__class__ == dict + assert input_str[0] in runtime_config.keys() + assert runtime_config[input_str[0]] == input_str[1] + + def test_deserialize_multiple_datatypes(self): + cfg = make_yaml_valid_dict(["linewrap", "23"]) + assert cfg["linewrap"] == 23 + + cfg = make_yaml_valid_dict(["encrypt", "false"]) + assert cfg["encrypt"] == False + + cfg = make_yaml_valid_dict(["editor", "vi -c startinsert"]) + assert cfg["editor"] == "vi -c startinsert" + + cfg = make_yaml_valid_dict(["highlight", "true"]) + assert cfg["highlight"] == True