Merge branch 'develop' into #1170-alternate-config-file

This commit is contained in:
Micah Jerome Ellison 2021-10-09 13:12:45 -07:00
commit 70aa5989ea
133 changed files with 706 additions and 4869 deletions

View file

@ -17,8 +17,8 @@ Feature: Delete entries from journal
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
# | basic_folder.yaml | @todo
# | basic_dayone.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: Backing out of interactive delete does not change journal
@ -66,7 +66,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -82,7 +82,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -98,7 +98,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -114,7 +114,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -130,7 +130,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -146,7 +146,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -162,7 +162,7 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
@ -178,5 +178,5 @@ Feature: Delete entries from journal
Examples: Configs
| config_file |
| basic_onefile.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo

View file

@ -28,15 +28,37 @@ Feature: Encrypting and decrypting journals
# This should warn the user that the journal is already encrypted
Scenario: Encrypting a journal
Scenario Outline: Encrypting a journal
Given we use the config "simple.yaml"
When we run "jrnl --encrypt" and enter
swordfish
swordfish
n
Then we should see the message "Journal encrypted"
Then we should get no error
And we should see the message "Journal encrypted"
And the config for journal "default" should contain "encrypt: true"
When we run "jrnl -n 1" and enter "swordfish"
Then we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"
Scenario Outline: Running jrnl with encrypt: true on unencryptable journals
Given we use the config "<config_file>"
When we run "jrnl --config-override encrypt true here is a new entry"
Then the error output should contain "this type of journal can't be encrypted"
Examples: configs
| config_file |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Attempt to encrypt a folder or DayOne journal should result in an error
Given we use the config "<config_file>"
When we run "jrnl --encrypt"
Then the error output should contain "can't be encrypted"
Examples: configs
| config_file |
| basic_folder.yaml |
| basic_dayone.yaml |

View file

@ -1,5 +1,33 @@
Feature: Custom formats
Scenario Outline: Short printing via --format flag
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --format short -3"
Then we should get no error
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Pretty Printing aka the Default
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --format pretty -3"
Then we should get no error
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: JSON format
Given we use the config "<config_file>"
And we use the password "test" if prompted
@ -296,6 +324,22 @@ Feature: Custom formats
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: Export fancy with small linewrap
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --config-override linewrap 35 --format fancy -3"
Then we should get no error
And the output should be 35 columns wide
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
@todo
Scenario Outline: Exporting fancy
# Needs better emoji support

View file

@ -11,7 +11,7 @@ Feature: Importing data
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: --import allows new large entry from stdin
@ -34,7 +34,7 @@ Feature: Importing data
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
Scenario Outline: --import allows multiple new entries from stdin
@ -56,7 +56,7 @@ Feature: Importing data
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
# | basic_folder.yaml | @todo
| basic_folder.yaml |
# | basic_dayone.yaml | @todo
Scenario: --import allows import new entries from file

View file

@ -88,3 +88,9 @@ Feature: Multiple journals
these three eyes
n
Then the output should contain "Encrypted journal 'new_encrypted' created"
Scenario: Read and write to journal with emoji name
Given we use the config "multiple.yaml"
When we run "jrnl Adding entry to sparkly journal"
When we run "jrnl -1"
Then the output should contain "Adding entry to sparkly journal"

View file

@ -0,0 +1,90 @@
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
And the editor should not 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 in memory should contain "highlight: false"
Then the editor should not 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 in memory should contain "colors.body: 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 in memory should contain
editor: nano
colors:
title: none
body: green
tags: none
date: none
Scenario: Override default journal
Given we use the config "basic_dayone.yaml"
And we use the password "test" if prompted
When we run "jrnl --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 --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.

View file

@ -10,6 +10,7 @@ scenarios("features/file_storage.feature")
scenarios("features/format.feature")
scenarios("features/import.feature")
scenarios("features/multiple_journals.feature")
scenarios("features/override.feature")
scenarios("features/password.feature")
scenarios("features/search.feature")
scenarios("features/star.feature")

View file

@ -12,6 +12,7 @@ journals:
new_encrypted:
encrypt: true
journal: features/journals/new_encrypted.journal
: features/journals/simple.journal
linewrap: 80
tagsymbols: '@'
timeformat: '%Y-%m-%d %H:%M'

View file

@ -143,10 +143,15 @@ def user_input():
@fixture
def config_data(config_path):
def config_on_disk(config_path):
return load_config(config_path)
@fixture
def config_in_memory():
return dict()
@fixture
def journal_name():
return None

View file

@ -1,6 +1,7 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
import functools
import os
@ -38,3 +39,13 @@ def assert_equal_tags_ignoring_order(
[actual_tags, expected_tags],
[expected_content, actual_content],
]
# @see: https://stackoverflow.com/a/65782539/569146
def get_nested_val(dictionary, path, *default):
try:
return functools.reduce(lambda x, y: x[y], path.split("."), dictionary)
except KeyError:
if default:
return default[0]
raise

View file

@ -15,6 +15,7 @@ from jrnl.config import scope_config
from .helpers import assert_equal_tags_ignoring_order
from .helpers import does_directory_contain_files
from .helpers import parse_should_or_should_not
from .helpers import get_nested_val
@then("we should get no error")
@ -84,6 +85,14 @@ def output_should_contain_version(cli_run, toml_version):
assert toml_version in out, toml_version
@then(parse("the output should be {width:d} columns wide"))
def output_should_be_columns_wide(cli_run, width):
out = cli_run["stdout"]
out_lines = out.splitlines()
for line in out_lines:
assert len(line) <= width
@then(parse('we should see the message "{text}"'))
def should_see_the_message(text, cli_run):
out = cli_run["stderr"]
@ -102,17 +111,18 @@ def should_see_the_message(text, cli_run):
)
@then(parse('the config {should_or_should_not} contain "{some_yaml}"'))
@then(parse("the config {should_or_should_not} contain\n{some_yaml}"))
def config_var(config_data, journal_name, should_or_should_not, some_yaml):
def config_var_on_disk(config_on_disk, journal_name, should_or_should_not, some_yaml):
we_should = parse_should_or_should_not(should_or_should_not)
actual = config_data
actual = config_on_disk
if journal_name:
actual = actual["journals"][journal_name]
expected = yaml.load(some_yaml, Loader=yaml.FullLoader)
expected = yaml.load(some_yaml, Loader=yaml.SafeLoader)
actual_slice = actual
if type(actual) is dict:
# `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: actual.get(key, None) for key in expected.keys()}
if we_should:
@ -121,6 +131,40 @@ def config_var(config_data, journal_name, should_or_should_not, some_yaml):
assert expected != actual_slice
@then(
parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain "{some_yaml}"'
)
)
@then(
parse(
'the config in memory for journal "{journal_name}" {should_or_should_not} contain\n{some_yaml}'
)
)
@then(parse('the config in memory {should_or_should_not} contain "{some_yaml}"'))
@then(parse("the config in memory {should_or_should_not} contain\n{some_yaml}"))
def config_var_in_memory(
config_in_memory, journal_name, should_or_should_not, some_yaml
):
we_should = parse_should_or_should_not(should_or_should_not)
actual = config_in_memory["overrides"]
if journal_name:
actual = actual["journals"][journal_name]
expected = yaml.load(some_yaml, Loader=yaml.SafeLoader)
actual_slice = actual
if type(actual) is dict:
# `expected` objects formatted in yaml only compare one level deep
actual_slice = {key: get_nested_val(actual, key) for key in expected.keys()}
if we_should:
assert expected == actual_slice
else:
assert expected != actual_slice
@then("we should be prompted for a password")
def password_was_called(cli_run):
assert cli_run["mocks"]["getpass"].called
@ -137,15 +181,15 @@ def assert_dir_contains_files(file_list, cache_dir):
@then(parse("the journal directory should contain\n{file_list}"))
def journal_directory_should_contain(config_data, file_list):
scoped_config = scope_config(config_data, "default")
def journal_directory_should_contain(config_on_disk, file_list):
scoped_config = scope_config(config_on_disk, "default")
assert does_directory_contain_files(file_list, scoped_config["journal"])
@then(parse('journal "{journal_name}" should not exist'))
def journal_directory_should_not_exist(config_data, journal_name):
scoped_config = scope_config(config_data, journal_name)
def journal_directory_should_not_exist(config_on_disk, journal_name):
scoped_config = scope_config(config_on_disk, journal_name)
assert not does_directory_contain_files(
scoped_config["journal"], "."
@ -153,8 +197,8 @@ def journal_directory_should_not_exist(config_data, journal_name):
@then(parse("the journal {should_or_should_not} exist"))
def journal_should_not_exist(config_data, should_or_should_not):
scoped_config = scope_config(config_data, "default")
def journal_should_not_exist(config_on_disk, should_or_should_not):
scoped_config = scope_config(config_on_disk, "default")
expected_path = scoped_config["journal"]
contains_files = does_directory_contain_files(expected_path, ".")
@ -301,15 +345,34 @@ def count_elements(number, item, cli_run):
assert len(xml_tree.findall(".//" + item)) == number
@then(parse("the editor should have been called"))
@then(parse("the editor should have been called with {num_args} arguments"))
def count_editor_args(num_args, cli_run, editor_state):
assert cli_run["mocks"]["editor"].called
@then(parse("the editor {should_or_should_not} have been called"))
@then(
parse(
"the editor {should_or_should_not} have been called with {num_args} arguments"
)
)
def count_editor_args(num_args, cli_run, editor_state, should_or_should_not):
we_should = parse_should_or_should_not(should_or_should_not)
if we_should:
assert cli_run["mocks"]["editor"].called
else:
assert not cli_run["mocks"]["editor"].called
if isinstance(num_args, int):
assert len(editor_state["command"]) == int(num_args)
@then(parse("the stdin prompt {should_or_should_not} have been called"))
def stdin_prompt_called(cli_run, should_or_should_not):
we_should = parse_should_or_should_not(should_or_should_not)
if we_should:
assert cli_run["mocks"]["stdin"].called
else:
assert not cli_run["mocks"]["stdin"].called
@then(parse('the editor filename should end with "{suffix}"'))
def editor_filename_suffix(suffix, editor_state):
editor_filename = editor_state["tmpfile"]["name"]

View file

@ -34,6 +34,7 @@ def when_we_change_directory(directory_name):
def we_run(
command,
config_path,
config_in_memory,
user_input,
cli_run,
capsys,
@ -63,7 +64,19 @@ def we_run(
password = user_input
with ExitStack() as stack:
# Always mock
from jrnl.override import apply_overrides
def my_overrides(*args, **kwargs):
result = apply_overrides(*args, **kwargs)
config_in_memory["overrides"] = result
return result
stack.enter_context(
patch("jrnl.jrnl.apply_overrides", side_effect=my_overrides)
)
# Conditionally mock
stack.enter_context(patch("sys.argv", ["jrnl"] + args))
mock_stdin = stack.enter_context(

View file

@ -6,6 +6,8 @@ from jrnl.override import _get_key_and_value_from_pair
from jrnl.override import _recursively_apply
from jrnl.override import apply_overrides
from argparse import Namespace
@pytest.fixture()
def minimal_config():
@ -18,31 +20,61 @@ def minimal_config():
return cfg
def expected_args(overrides):
default_args = {
"contains": None,
"debug": False,
"delete": False,
"edit": False,
"end_date": None,
"today_in_history": False,
"month": None,
"day": None,
"year": None,
"excluded": [],
"export": False,
"filename": None,
"limit": None,
"on_date": None,
"preconfig_cmd": None,
"postconfig_cmd": None,
"short": False,
"starred": False,
"start_date": None,
"strict": False,
"tags": False,
"text": [],
"config_override": [],
}
return Namespace(**{**default_args, **overrides})
def test_apply_override(minimal_config):
overrides = [["editor", "nano"]]
apply_overrides(overrides, minimal_config)
overrides = {"config_override": [["editor", "nano"]]}
apply_overrides(expected_args(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"}
overrides = {"config_override": [["colors.body", "blue"]]}
apply_overrides(expected_args(overrides), minimal_config)
assert minimal_config["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 = {
"config_override": [
["colors.title", "magenta"],
["editor", "nano"],
["journals.burner", "/tmp/journals/burner.jrnl"],
]
}
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"
actual = apply_overrides(expected_args(overrides), minimal_config)
assert actual["editor"] == "nano"
assert actual["colors"]["title"] == "magenta"
assert "burner" in actual["journals"]
assert actual["journals"]["burner"] == "/tmp/journals/burner.jrnl"
def test_recursively_apply():