Rework how all output and messaging works in jrnl (#1475)

* fix missed statement from last PR
* replace print statement for adding an entry to a journal
* clean up linting and format
* change print statement over to new print_msg function
* make print_msg always print to stderr
* change print statement over to new print_msg function
* update importer to use new message function
* update yaml format to use new message function
* code cleanup
* update yaml format to use new message function
* update yaml format to use new exception handling
* update Journal class to use new message function
* update install module to use new message function
* update config module to use new message function
* update upgrade module to properly use new message and exception handling
* fix typo
* update upgrade module to use new message handling
* update welcome message to use new handling
* update upgrade module to use new message handling
* update upgrade module journal summaries to use new message handling
* take out old code
* update upgrade module to use new message handling
* update upgrade module to use new message handling
* update more modules to use new message handling
* take out old comment
* update deprecated_cmd to use new message handling
* update text_exporter with new message handling, get rid of old color constants
* get rid of hardcoded text
* whitespace changes
* rework MsgType into MsgStyle so messages can have different styles
* add comment
* Move around code to separate concerns of each function a bit more
* update create_password and yesno prompt functions for new messaging
* fix missing newline for keyboard interrupts
* fix misc linting
* fix bug with panel titles always showing 'error' after one error
* fix missing import
* update debug output after uncaught exception
* update exception for new exception handling
* rewrite yesno function to use new centralized messages
* reduce the debug output slightly
* clean up print_msgs function
* clean up create_password function
* clean up misc linting
* rename screen_input to hide_input to be more clear
* update encrypted journal prompt to use new messaging functionality
* fix typo in message key
* move rich console into function so we can mock properly
* update password mock to use rich console instead of getpass
* add more helpful output to then step
* fix test by updating expected output
* update message to use new functionality
* rework mocks in test suite for new messaging functionality
* fix linting issue
* fix more tests
* fix more tests
* fix more tests
* fix more tests
* fix merge bug
* update prompt_action_entries to use new messaging functionality
* Add new input_method "type"
  This does the same thing as input_method "pipe" but is more clear what
  it's doing (typing text into the builtin composer)
* get rid of old commented code
* get rid of unused code
* move some files around

Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
Jonathan Wren 2022-06-11 13:32:11 -07:00 committed by GitHub
parent 4d683a13c0
commit f53110c69b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 912 additions and 470 deletions

View file

@ -73,7 +73,7 @@ Feature: Multiple journals
these three eyes
these three eyes
n
Then the output should contain "Encrypted journal 'new_encrypted' created"
Then the output should contain "Journal 'new_encrypted' created at "
Scenario: Don't overwrite main config when encrypting a journal in an alternate config
Given the config "basic_onefile.yaml" exists

View file

@ -69,7 +69,7 @@ Feature: Reading and writing to journal with custom date formats
Scenario: Writing an entry at the prompt with custom date
Given we use the config "little_endian_dates.yaml"
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
When we run "jrnl" and type "2013-05-10: I saw Elvis. He's alive."
Then we should get no error
When we run "jrnl -999"
Then the output should contain "10.05.2013 09:00 I saw Elvis."

View file

@ -2,8 +2,8 @@ Feature: Encrypting and decrypting journals
Scenario: Decrypting a journal
Given we use the config "encrypted.yaml"
# And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl --decrypt"
Then the output should contain "Journal decrypted"
And the config for journal "default" should contain "encrypt: false"
When we run "jrnl -99 --short"
@ -47,7 +47,7 @@ Feature: Encrypting and decrypting journals
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"
Then the error output should contain "journal can't be encrypted"
Examples: configs
| config_file |

View file

@ -429,7 +429,7 @@ Feature: Custom formats
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --export yaml --file nonexistent_dir"
Then the output should contain "YAML export must be to individual files"
Then the output should contain "YAML export must be to a directory"
And the output should not contain "Traceback"
Examples: configs

View file

@ -87,7 +87,7 @@ Feature: Multiple journals
these three eyes
these three eyes
n
Then the output should contain "Encrypted journal 'new_encrypted' created"
Then the output should contain "Journal 'new_encrypted' created at"
Scenario: Read and write to journal with emoji name
Given we use the config "multiple.yaml"

View file

@ -3,7 +3,7 @@ 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 ''" and enter
When we run "jrnl --config-override editor ''" and type
This is a journal entry
Then the stdin prompt should have been called
And the editor should not have been called

View file

@ -29,7 +29,8 @@ Feature: Starring entries
Scenario: Starring an entry will mark it in an encrypted journal
Given we use the config "encrypted.yaml"
When we run "jrnl 20 july 2013 *: Best day of my life!" and enter "bad doggie no biscuit"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl 20 july 2013 *: Best day of my life!"
Then the output should contain "Entry added"
When we run "jrnl -on 2013-07-20 -starred" and enter "bad doggie no biscuit"
Then the output should contain "2013-07-20 09:00 Best day of my life!"

View file

@ -41,7 +41,7 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
Scenario: Upgrade with missing journal
Given we use the config "upgrade_from_195_with_missing_journal.json"
When we run "jrnl --list" and enter "Y"
Then the output should contain "Error: features/journals/missing.journal does not exist."
Then the output should contain "features/journals/missing.journal does not exist"
And we should get no error
Scenario: Upgrade with missing encrypted journal
@ -49,6 +49,6 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
When we run "jrnl --list" and enter
Y
bad doggie no biscuit
Then the output should contain "Error: features/journals/missing.journal does not exist."
Then the output should contain "features/journals/missing.journal does not exist"
And the output should contain "We're all done"
And we should get no error

View file

@ -172,7 +172,7 @@ Feature: Writing new entries.
Scenario Outline: Writing an entry at the prompt (no editor) should store the entry
Given we use the config "<config_file>"
And we use the password "bad doggie no biscuit" if prompted
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
When we run "jrnl" and type "25 jul 2013: I saw Elvis. He's alive."
Then we should get no error
When we run "jrnl -on '2013-07-25'"
Then the output should contain "2013-07-25 09:00 I saw Elvis."
@ -233,8 +233,7 @@ Feature: Writing new entries.
And we append to the editor if opened
[2021-11-13] worked on jrnl tests
When we run "jrnl --edit"
Then the output should contain
[1 entry added]
Then the output should contain "1 entry added"
Examples: configs
| config_file |
@ -252,8 +251,7 @@ Feature: Writing new entries.
[2021-11-12] worked on jrnl tests again
[2021-11-13] worked on jrnl tests a little bit more
When we run "jrnl --edit"
Then the output should contain
[3 entries added]
Then the error output should contain "3 entries added"
Examples: configs
| config_file |
@ -269,8 +267,8 @@ Feature: Writing new entries.
And we write to the editor if opened
[2021-11-13] I am replacing my whole journal with this entry
When we run "jrnl --edit"
Then the output should contain
[2 entries deleted, 1 entry modified]
Then the output should contain "2 entries deleted"
Then the output should contain "3 entries modified"
Examples: configs
| config_file |
@ -287,7 +285,7 @@ Feature: Writing new entries.
[2021-11-13] I am replacing the last entry with this entry
When we run "jrnl --edit -1"
Then the output should contain
[1 entry modified]
1 entry modified
Examples: configs
| config_file |
@ -304,7 +302,7 @@ Feature: Writing new entries.
This is a small addendum to my latest entry.
When we run "jrnl --edit"
Then the output should contain
[1 entry modified]
1 entry modified
Examples: configs
| config_file |

View file

@ -6,12 +6,15 @@ import os
from pathlib import Path
import tempfile
from collections.abc import Iterable
from keyring import backend
from keyring import errors
from pytest import fixture
from unittest.mock import patch
from unittest.mock import Mock
from .helpers import get_fixture
import toml
from rich.console import Console
from jrnl.config import load_config
from jrnl.os_compat import split_args
@ -85,7 +88,6 @@ def cli_run(
mock_editor,
mock_user_input,
mock_overrides,
mock_password,
):
# Check if we need more mocks
mock_factories.update(mock_args)
@ -94,7 +96,6 @@ def cli_run(
mock_factories.update(mock_editor)
mock_factories.update(mock_config_path)
mock_factories.update(mock_user_input)
mock_factories.update(mock_password)
return {
"status": 0,
@ -180,26 +181,6 @@ def toml_version(working_dir):
return pyproject_contents["tool"]["poetry"]["version"]
@fixture
def mock_password(request):
def _mock_password():
password = get_fixture(request, "password")
user_input = get_fixture(request, "user_input")
if password:
password = password.splitlines()
elif user_input:
password = user_input.splitlines()
if not password:
password = Exception("Unexpected call for password")
return patch("getpass.getpass", side_effect=password)
return {"getpass": _mock_password}
@fixture
def input_method():
return ""
@ -221,30 +202,58 @@ def should_not():
@fixture
def mock_user_input(request, is_tty):
def _generator(target):
def _mock_user_input():
user_input = get_fixture(request, "user_input", None)
def mock_user_input(request, password_input, stdin_input):
def _mock_user_input():
# user_input needs to be here because we don't know it until cli_run starts
user_input = get_fixture(request, "all_input", None)
if user_input is None:
user_input = Exception("Unexpected call for user input")
else:
user_input = iter(user_input.splitlines())
if user_input is None:
user_input = Exception("Unexpected call for user input")
else:
user_input = user_input.splitlines() if is_tty else [user_input]
def mock_console_input(**kwargs):
if kwargs["password"] and not isinstance(password_input, Exception):
return password_input
return patch(target, side_effect=user_input)
if isinstance(user_input, Iterable):
return next(user_input)
return _mock_user_input
# exceptions
return user_input if not kwargs["password"] else password_input
mock_console = Mock(wraps=Console(stderr=True))
mock_console.input = Mock(side_effect=mock_console_input)
return patch("jrnl.output._get_console", return_value=mock_console)
return {
"stdin": _generator("sys.stdin.read"),
"input": _generator("builtins.input"),
"user_input": _mock_user_input,
"stdin_input": lambda: patch("sys.stdin.read", side_effect=stdin_input),
}
@fixture
def password_input(request):
password_input = get_fixture(request, "password", None)
if password_input is None:
password_input = Exception("Unexpected call for password input")
return password_input
@fixture
def stdin_input(request, is_tty):
stdin_input = get_fixture(request, "all_input", None)
if stdin_input is None or is_tty:
stdin_input = Exception("Unexpected call for stdin input")
else:
stdin_input = [stdin_input]
return stdin_input
@fixture
def is_tty(input_method):
assert input_method in ["", "enter", "pipe"]
return input_method != "pipe"
assert input_method in ["", "enter", "pipe", "type"]
return input_method not in ["pipe", "type"]
@fixture

View file

@ -120,9 +120,9 @@ def config_exists(config_file, temp_dir, working_dir):
shutil.copy2(config_source, config_dest)
@given(parse('we use the password "{pw}" if prompted'), target_fixture="password")
def use_password_forever(pw):
return pw
@given(parse('we use the password "{password}" if prompted'))
def use_password_forever(password):
return password
@given("we create a cache directory", target_fixture="cache_dir")

View file

@ -47,20 +47,23 @@ def output_should_contain(
):
we_should = parse_should_or_should_not(should_or_should_not)
output_str = f"\nEXPECTED:\n{expected_output}\n\nACTUAL STDOUT:\n{cli_run['stdout']}\n\nACTUAL STDERR:\n{cli_run['stderr']}"
assert expected_output
if which_output_stream is None:
assert ((expected_output in cli_run["stdout"]) == we_should) or (
(expected_output in cli_run["stderr"]) == we_should
)
), output_str
elif which_output_stream == "standard":
assert (expected_output in cli_run["stdout"]) == we_should
assert (expected_output in cli_run["stdout"]) == we_should, output_str
elif which_output_stream == "error":
assert (expected_output in cli_run["stderr"]) == we_should
assert (expected_output in cli_run["stderr"]) == we_should, output_str
else:
assert (expected_output in cli_run[which_output_stream]) == we_should
assert (
expected_output in cli_run[which_output_stream]
) == we_should, output_str
@then(parse("the output should not contain\n{expected_output}"))
@ -164,12 +167,12 @@ def config_var_in_memory(
@then("we should be prompted for a password")
def password_was_called(cli_run):
assert cli_run["mocks"]["getpass"].called
assert cli_run["mocks"]["user_input"].called
@then("we should not be prompted for a password")
def password_was_not_called(cli_run):
assert not cli_run["mocks"]["getpass"].called
assert not cli_run["mocks"]["user_input"].called
@then(parse("the cache directory should contain the files\n{file_list}"))
@ -371,7 +374,7 @@ def count_editor_args(num_args, cli_run, editor_state, should_or_should_not):
def stdin_prompt_called(cli_run, should_or_should_not):
we_should = parse_should_or_should_not(should_or_should_not)
assert cli_run["mocks"]["stdin"].called == we_should
assert cli_run["mocks"]["stdin_input"].called == we_should
@then(parse('the editor filename should end with "{suffix}"'))

View file

@ -21,12 +21,12 @@ def when_we_change_directory(directory_name):
# These variables are used in the `@when(re(...))` section below
command = '(?P<command>[^"]*)'
input_method = "(?P<input_method>enter|pipe)"
user_input = '("(?P<user_input>[^"]*)")'
input_method = "(?P<input_method>enter|pipe|type)"
all_input = '("(?P<all_input>[^"]*)")'
@when(parse('we run "jrnl {command}" and {input_method}\n{user_input}'))
@when(re(f'we run "jrnl ?{command}" and {input_method} {user_input}'))
@when(parse('we run "jrnl {command}" and {input_method}\n{all_input}'))
@when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}'))
@when(parse('we run "jrnl {command}"'))
@when('we run "jrnl"')
def we_run_jrnl(cli_run, capsys, keyring):

27
tests/unit/test_output.py Normal file
View file

@ -0,0 +1,27 @@
# Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html
from unittest.mock import Mock
from unittest.mock import patch
from jrnl.messages import Message
from jrnl.output import print_msg
@patch("jrnl.output.print_msgs")
def test_print_msg_calls_print_msgs_as_list_with_style(print_msgs):
test_msg = Mock(Message)
print_msg(test_msg)
print_msgs.assert_called_once_with([test_msg], style=test_msg.style)
@patch("jrnl.output.print_msgs")
def test_print_msg_calls_print_msgs_with_kwargs(print_msgs):
test_msg = Mock(Message)
kwargs = {
"delimter": "test delimiter 🤡",
"get_input": True,
"hide_input": True,
"some_rando_arg": "💩",
}
print_msg(test_msg, **kwargs)
print_msgs.assert_called_once_with([test_msg], style=test_msg.style, **kwargs)