Merge branch 'develop' of https://github.com/jrnl-org/jrnl into develop

This commit is contained in:
Seopril 2021-01-04 23:02:22 -05:00
commit ecafc2ce1b
22 changed files with 389 additions and 139 deletions

View file

@ -104,7 +104,7 @@ jobs:
issuesWoLabels: false issuesWoLabels: false
unreleased: true unreleased: true
compareLink: true compareLink: true
includeLabels: bug,enhancement,documentation,build,deprecated includeLabels: bug,enhancement,documentation,build,packaging,deprecated
excludeLabels: stale,wontfix excludeLabels: stale,wontfix
excludeTagsRegex: ${{ env.TAG_REGEX }} excludeTagsRegex: ${{ env.TAG_REGEX }}
sinceTag: ${{ env.SINCE_TAG }} sinceTag: ${{ env.SINCE_TAG }}

View file

@ -61,7 +61,7 @@ jobs:
env: env:
site_url: http://127.0.0.1:8000 site_url: http://127.0.0.1:8000
run: | run: |
select="{urls: [\"${site_url}/\", .urlset.url[].loc]}" select="{urls: [\"${site_url}/\", \"${site_url}/search.html?q=jrnl\", .urlset.url[].loc]}"
curl -s "$site_url/sitemap.xml" | poetry run xq "$select" > list.json curl -s "$site_url/sitemap.xml" | poetry run xq "$select" > list.json
- name: Accessibility testing (Pa11y) - name: Accessibility testing (Pa11y)

View file

@ -7,6 +7,14 @@
**Implemented enhancements:** **Implemented enhancements:**
- Implement dependency tracker/updater [\#1120](https://github.com/jrnl-org/jrnl/issues/1120) - Implement dependency tracker/updater [\#1120](https://github.com/jrnl-org/jrnl/issues/1120)
- Change temporary file names for better text editor integration [\#1080](https://github.com/jrnl-org/jrnl/issues/1080)
- Allow custom file extension for `jrnl --edit` command [\#1059](https://github.com/jrnl-org/jrnl/issues/1059)
- Allow custom extensions when editing \(for easier syntax highlighting\) [\#1139](https://github.com/jrnl-org/jrnl/pull/1139) ([KarimPwnz](https://github.com/KarimPwnz))
**Fixed bugs:**
- Error if password exists in keyring, but retrieval fails for any reason [\#1020](https://github.com/jrnl-org/jrnl/issues/1020)
- Fix keyring error handling [\#1138](https://github.com/jrnl-org/jrnl/pull/1138) ([KarimPwnz](https://github.com/KarimPwnz))
**Build:** **Build:**
@ -14,10 +22,18 @@
**Documentation:** **Documentation:**
- Fix broken search bar in docs site [\#1135](https://github.com/jrnl-org/jrnl/pull/1135) ([wren](https://github.com/wren))
- Fix search on docs site [\#1133](https://github.com/jrnl-org/jrnl/pull/1133) ([wren](https://github.com/wren)) - Fix search on docs site [\#1133](https://github.com/jrnl-org/jrnl/pull/1133) ([wren](https://github.com/wren))
- Add packaging label to changelog generator config [\#1132](https://github.com/jrnl-org/jrnl/pull/1132) ([wren](https://github.com/wren)) - Add packaging label to changelog generator config [\#1132](https://github.com/jrnl-org/jrnl/pull/1132) ([wren](https://github.com/wren))
- Fix failing contrast test in accessibility tools on docs site [\#1126](https://github.com/jrnl-org/jrnl/pull/1126) ([wren](https://github.com/wren)) - Fix failing contrast test in accessibility tools on docs site [\#1126](https://github.com/jrnl-org/jrnl/pull/1126) ([wren](https://github.com/wren))
**Packaging:**
- Bump keyring from 21.7.0 to 21.8.0 [\#1136](https://github.com/jrnl-org/jrnl/pull/1136) ([dependabot[bot]](https://github.com/apps/dependabot))
- Bump pytz from 2020.4 to 2020.5 [\#1130](https://github.com/jrnl-org/jrnl/pull/1130) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump pytest from 6.2.0 to 6.2.1 [\#1129](https://github.com/jrnl-org/jrnl/pull/1129) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
- Bump keyring from 21.5.0 to 21.7.0 [\#1128](https://github.com/jrnl-org/jrnl/pull/1128) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
## [v2.6](https://pypi.org/project/jrnl/v2.6/) (2020-12-20) ## [v2.6](https://pypi.org/project/jrnl/v2.6/) (2020-12-20)
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.5...v2.6) [Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.5...v2.6)

View file

@ -15,6 +15,7 @@
--yellow: #e2b93d; --yellow: #e2b93d;
/* For light bg */ /* For light bg */
--black: #404040;
--teal: #2a8068; --teal: #2a8068;
--dark-blue: #356eb7; --dark-blue: #356eb7;
--mid-purple: #846392; --mid-purple: #846392;

View file

@ -8,6 +8,7 @@
body.wy-body-for-nav, body.wy-body-for-nav,
section.wy-nav-content-wrap { section.wy-nav-content-wrap {
background-color: var(--white); background-color: var(--white);
color: var(--black);
} }
.rst-content pre { .rst-content pre {
@ -102,7 +103,7 @@ a.icon-home:before {
.wy-menu-vertical a, .wy-menu-vertical a,
.wy-menu-vertical li ul li a { .wy-menu-vertical li ul li a {
font-size: 16px; font-size: 16px;
color: var(--off-white); color: var(--white);
line-height: 2em; line-height: 2em;
} }
@ -167,7 +168,7 @@ a.icon-home:before {
} }
.wy-menu-vertical li a { .wy-menu-vertical li a {
color: var(--off-white) !important; color: var(--white) !important;
font-weight: 300; font-weight: 300;
} }
@ -185,18 +186,20 @@ footer {
} }
.wy-side-nav-search input[type=text], .wy-side-nav-search input[type=text],
.mkdocs-search input[type=text],
form .search-query { form .search-query {
background-color: var(--black-shadow) !important; background-color: var(--off-white);
border: none; border: none;
box-shadow: none;
margin-bottom: 1em; margin-bottom: 1em;
color: var(--white); color: var(--black);
font-weight: 500; font-weight: 500;
box-shadow: none;
} }
.wy-side-nav-search input[type=text]::placeholder, .wy-side-nav-search input[type=text]::placeholder,
.mkdocs-search input[type=text]::placeholder,
form .search-query::placeholder { form .search-query::placeholder {
color: var(--off-white); color: var(--dark-purple);
} }
.wy-side-nav-search > a:hover { .wy-side-nav-search > a:hover {
@ -298,19 +301,54 @@ ol>li:before {
margin-top: 20px; margin-top: 20px;
} }
.wy-side-nav-search input[type="text"] { .wy-side-nav-search input[type="text"],
.mkdocs-search input[type=text] {
border-radius: 50px 0 0 50px; border-radius: 50px 0 0 50px;
height: 32px; height: 32px;
border-right: none; border-right: none;
margin: 0;
} }
.mkdocs-search button { .mkdocs-search button {
background-color: var(--black-shadow); background-color: var(--off-white);
border: none; border: none;
box-shadow: none; box-shadow: none;
color: var(--white); color: var(--mid-purple);
border-radius: 0 50px 50px 0; border-radius: 0 50px 50px 0;
height: 32px; height: 32px;
width: 2.5em; width: 2.5em;
overflow: hidden; overflow: hidden;
} }
.mkdocs-search {
border-radius: 50px;
}
.mkdocs-search:focus-within {
box-shadow: 0 2px 25px 0 var(--blacker-shadow);
transition: all .5s ease;
}
.rst-content div[role="main"] .mkdocs-search input[type="text"] {
border-right: none;
font-size: 100%;
height: 48px;
margin: 0;
}
.rst-content div[role="main"] .mkdocs-search button {
border-left: none;
font-size: 100%;
height: 48px;
}
.rst-content div[role="main"] .mkdocs-search button:before {
font-size: 140%;
position: relative;
left: -7px;
top: -1px;
}
.search-results {
margin-top: 0;
}

View file

@ -1,8 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block search_button %} {%- block search_button %}
<form class="mkdocs-search" action="{{ base_url }}/search.html"> {% if 'search' in config['plugins'] %}
<input type="text" name="q" placeholder="Search docs" title="Type search term here"> <div role="search">
<button class="icon icon-search" aria-label="submit"></button> <form id ="rtd-search-form" class="wy-form mkdocs-search" action="{{ base_url }}/search.html" method="get">
</form> <input type="text" name="q" placeholder="Search docs" title="Type search term here" />
{% endblock %} <button class="icon icon-search" aria-label="submit"></button>
</form>
</div>
{% endif %}
{%- endblock %}

29
docs_theme/search.html Normal file
View file

@ -0,0 +1,29 @@
{% extends "main.html" %}
{% block content %}
<div role="search">
<form id ="content_search" class="wy-form mkdocs-search" action="{{ base_url }}/search.html" method="get">
<span role="status" aria-live="polite" class="ui-helper-hidden-accessible"></span>
<input
name="q"
id="mkdocs-search-query"
type="text"
class="search_input search-query ui-autocomplete-input"
placeholder="Search the Docs"
autocomplete="off"
autofocus
title="Type search term here"
>
<button class="icon icon-search" aria-label="submit"></button>
</form>
</div>
<h1 id="search">Results</h1>
<div id="mkdocs-search-results" class="search-results">
Searching...
</div>
{% endblock %}

View file

@ -0,0 +1,18 @@
default_hour: 9
default_minute: 0
editor: ""
encrypt: false
highlight: true
editor: "vim"
journals:
default: features/journals/editor_markdown_extension.journal
linewrap: 80
tagsymbols: "@"
template: features/templates/extension.md
timeformat: "%Y-%m-%d %H:%M"
indent_character: "|"
colors:
date: none
title: none
body: none
tags: none

View file

View file

@ -44,3 +44,13 @@ Feature: Journals iteracting with the file system in a way that users can see
And we change directory to "features" And we change directory to "features"
And we run "jrnl -n 1" And we run "jrnl -n 1"
Then the output should contain "hello world" Then the output should contain "hello world"
Scenario: the temporary filename suffix should default to ".jrnl"
Given we use the config "editor.yaml"
When we run "jrnl --edit"
Then the temporary filename suffix should be ".jrnl"
Scenario: the temporary filename suffix should be "-{template_filename}"
Given we use the config "editor_markdown_extension.yaml"
When we run "jrnl --edit"
Then the temporary filename suffix should be "-extension.md"

View file

@ -24,6 +24,7 @@ Feature: Using the installed keyring
n n
""" """
Then we should get no error Then we should get no error
And we should not see the message "Failed to retrieve keyring"
Scenario: Encrypt journal with no keyring backend and do store in keyring Scenario: Encrypt journal with no keyring backend and do store in keyring
Given we use the config "simple.yaml" Given we use the config "simple.yaml"
@ -36,25 +37,53 @@ Feature: Using the installed keyring
y y
""" """
Then we should get no error Then we should get no error
And we should not see the message "Failed to retrieve keyring"
# @todo add step to check contents of keyring # @todo add step to check contents of keyring
@todo @todo
Scenario: Open an encrypted journal with wrong password in keyring Scenario: Open an encrypted journal with wrong password in keyring
# This should ask the user for the password after the keyring fails # This should ask the user for the password after the keyring fails
@todo
Scenario: Open encrypted journal when keyring exists but fails
# This should ask the user for the password after the keyring fails
@todo @todo
Scenario: Decrypt journal with password in keyring Scenario: Decrypt journal with password in keyring
@todo @todo
Scenario: Decrypt journal without a keyring Scenario: Decrypt journal without a keyring
@todo Scenario: Encrypt journal when keyring exists but fails
Given we use the config "simple.yaml"
And we have a failed keyring
When we run "jrnl --encrypt" and enter
"""
this password will not be saved in keyring
this password will not be saved in keyring
y
"""
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And the config for journal "default" should have "encrypt" set to "bool:True"
Scenario: Decrypt journal when keyring exists but fails Scenario: Decrypt journal when keyring exists but fails
Given we use the config "encrypted.yaml"
And we have a failed keyring
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And we should see the message "Journal decrypted"
And the config for journal "default" should have "encrypt" set to "bool:False"
And the journal should have 2 entries
Scenario: Open encrypted journal when keyring exists but fails
# This should ask the user for the password after the keyring fails # This should ask the user for the password after the keyring fails
Given we use the config "encrypted.yaml"
And we have a failed keyring
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
Then we should see the message "Failed to retrieve keyring"
And we should get no error
And we should be prompted for a password
And the output should contain "2013-06-10 15:40 Life is good"
Scenario: Mistyping your password Scenario: Mistyping your password
Given we use the config "simple.yaml" Given we use the config "simple.yaml"

View file

@ -19,7 +19,6 @@ import yaml
from jrnl import Journal from jrnl import Journal
from jrnl import __version__ from jrnl import __version__
from jrnl import install
from jrnl import plugins from jrnl import plugins
from jrnl.cli import cli from jrnl.cli import cli
from jrnl.config import load_config from jrnl.config import load_config
@ -69,21 +68,19 @@ class NoKeyring(keyring.backend.KeyringBackend):
class FailedKeyring(keyring.backend.KeyringBackend): class FailedKeyring(keyring.backend.KeyringBackend):
""" """
A keyring that simulates an environment with a keyring that has passwords, but fails A keyring that cannot be retrieved.
to return them.
""" """
priority = 2 priority = 2
keys = defaultdict(dict)
def set_password(self, servicename, username, password): def set_password(self, servicename, username, password):
self.keys[servicename][username] = password raise keyring.errors.KeyringError
def get_password(self, servicename, username): def get_password(self, servicename, username):
raise keyring.errors.NoKeyringError raise keyring.errors.KeyringError
def delete_password(self, servicename, username): def delete_password(self, servicename, username):
self.keys[servicename][username] = None raise keyring.errors.KeyringError
# set a default keyring # set a default keyring
@ -94,25 +91,25 @@ def ushlex(command):
return shlex.split(command, posix=not on_windows) return shlex.split(command, posix=not on_windows)
def read_journal(journal_name="default"): def read_journal(context, journal_name="default"):
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
with open(config["journals"][journal_name]) as journal_file: with open(configuration["journals"][journal_name]) as journal_file:
journal = journal_file.read() journal = journal_file.read()
return journal return journal
def open_journal(journal_name="default"): def open_journal(context, journal_name="default"):
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
journal_conf = config["journals"][journal_name] journal_conf = configuration["journals"][journal_name]
# We can override the default config on a by-journal basis # We can override the default config on a by-journal basis
if type(journal_conf) is dict: if type(journal_conf) is dict:
config.update(journal_conf) configuration.update(journal_conf)
# But also just give them a string to point to the journal file # But also just give them a string to point to the journal file
else: else:
config["journal"] = journal_conf configuration["journal"] = journal_conf
return Journal.open_journal(journal_name, config) return Journal.open_journal(journal_name, configuration)
def read_value_from_string(string): def read_value_from_string(string):
@ -130,10 +127,11 @@ def read_value_from_string(string):
def set_config(context, config_file): def set_config(context, config_file):
full_path = os.path.join("features/configs", config_file) full_path = os.path.join("features/configs", config_file)
install.CONFIG_FILE_PATH = os.path.abspath(full_path) context.config_path = os.path.abspath(full_path)
if config_file.endswith("yaml") and os.path.exists(full_path): if config_file.endswith("yaml") and os.path.exists(full_path):
# Add jrnl version to file for 2.x journals # Add jrnl version to file for 2.x journals
with open(install.CONFIG_FILE_PATH, "a") as cf: with open(context.config_path, "a") as cf:
cf.write("version: {}".format(__version__)) cf.write("version: {}".format(__version__))
@ -148,8 +146,12 @@ def use_password(context, password, num=1):
@given("we have a keyring") @given("we have a keyring")
def set_keyring(context): @given("we have a {type} keyring")
keyring.set_keyring(TestKeyring()) def set_keyring(context, type=""):
if type == "failed":
keyring.set_keyring(FailedKeyring())
else:
keyring.set_keyring(TestKeyring())
@given("we do not have a keyring") @given("we do not have a keyring")
@ -194,11 +196,18 @@ def open_editor_and_enter(context, method, text=""):
with \ with \
patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \ patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \ patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
patch("sys.stdin.isatty", return_value=True) \ patch("sys.stdin.isatty", return_value=True), \
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
: :
context.editor = mock_editor context.editor = mock_editor
context.getpass = mock_getpass context.getpass = mock_getpass
cli(["--edit"]) try:
cli(["--edit"])
context.exit_status = 0
except SystemExit as e:
context.exit_status = e.code
# fmt: on # fmt: on
@ -248,6 +257,14 @@ def contains_editor_file(context, method, text=""):
assert False, f"Method '{method}' not supported" assert False, f"Method '{method}' not supported"
@then('the temporary filename suffix should be "{suffix}"')
def extension_editor_file(context, suffix):
filename = Path(context.editor_file["name"]).name
delimiter = "-" if "-" in filename else "."
filename_suffix = delimiter + filename.split(delimiter)[-1]
assert filename_suffix == suffix
def _mock_getpass(inputs): def _mock_getpass(inputs):
def prompt_return(prompt=""): def prompt_return(prompt=""):
if type(inputs) == str: if type(inputs) == str:
@ -304,7 +321,9 @@ def run_with_input(context, command, inputs=""):
patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \ patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \ patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
patch("sys.stdin.read", side_effect=text) as mock_read, \ patch("sys.stdin.read", side_effect=text) as mock_read, \
patch("subprocess.call", side_effect=_mock_editor) as mock_editor \ patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
: :
try: try:
cli(args or []) cli(args or [])
@ -386,7 +405,9 @@ def run(context, command, text=""):
patch("sys.argv", args), \ patch("sys.argv", args), \
patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \ patch("getpass.getpass", side_effect=_mock_getpass(password)) as mock_getpass, \
patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \ patch("subprocess.call", side_effect=_mock_editor) as mock_editor, \
patch("sys.stdin.read", side_effect=lambda: text) \ patch("sys.stdin.read", side_effect=lambda: text), \
patch("jrnl.config.get_config_path", side_effect=lambda: context.config_path), \
patch("jrnl.install.get_config_path", side_effect=lambda: context.config_path) \
: :
context.editor = mock_editor context.editor = mock_editor
context.getpass = mock_getpass context.getpass = mock_getpass
@ -534,32 +555,32 @@ def check_not_message(context, text):
@then('the journal should contain "{text}"') @then('the journal should contain "{text}"')
@then('journal "{journal_name}" should contain "{text}"') @then('journal "{journal_name}" should contain "{text}"')
def check_journal_content(context, text, journal_name="default"): def check_journal_content(context, text, journal_name="default"):
journal = read_journal(journal_name) journal = read_journal(context, journal_name)
assert text in journal, journal assert text in journal, journal
@then('the journal should not contain "{text}"') @then('the journal should not contain "{text}"')
@then('journal "{journal_name}" should not contain "{text}"') @then('journal "{journal_name}" should not contain "{text}"')
def check_not_journal_content(context, text, journal_name="default"): def check_not_journal_content(context, text, journal_name="default"):
journal = read_journal(journal_name) journal = read_journal(context, journal_name)
assert text not in journal, journal assert text not in journal, journal
@then("the journal should not exist") @then("the journal should not exist")
@then('journal "{journal_name}" should not exist') @then('journal "{journal_name}" should not exist')
def journal_doesnt_exist(context, journal_name="default"): def journal_doesnt_exist(context, journal_name="default"):
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
journal_path = config["journals"][journal_name] journal_path = configuration["journals"][journal_name]
assert not os.path.exists(journal_path) assert not os.path.exists(journal_path)
@then("the journal should exist") @then("the journal should exist")
@then('journal "{journal_name}" should exist') @then('journal "{journal_name}" should exist')
def journal_exists(context, journal_name="default"): def journal_exists(context, journal_name="default"):
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
journal_path = config["journals"][journal_name] journal_path = configuration["journals"][journal_name]
assert os.path.exists(journal_path) assert os.path.exists(journal_path)
@ -568,23 +589,23 @@ def journal_exists(context, journal_name="default"):
@then('the config for journal "{journal}" 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): def config_var(context, key, value="", journal=None):
value = read_value_from_string(value or context.text or "") value = read_value_from_string(value or context.text or "")
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
if journal: if journal:
config = config["journals"][journal] configuration = configuration["journals"][journal]
assert key in config assert key in configuration
assert config[key] == value assert configuration[key] == value
@then('the config for journal "{journal}" should not have "{key}" set') @then('the config for journal "{journal}" should not have "{key}" set')
def config_no_var(context, key, value="", journal=None): def config_no_var(context, key, value="", journal=None):
config = load_config(install.CONFIG_FILE_PATH) configuration = load_config(context.config_path)
if journal: if journal:
config = config["journals"][journal] configuration = configuration["journals"][journal]
assert key not in config assert key not in configuration
@then("the journal should have {number:d} entries") @then("the journal should have {number:d} entries")
@ -592,15 +613,15 @@ def config_no_var(context, key, value="", journal=None):
@then('journal "{journal_name}" should have {number:d} entries') @then('journal "{journal_name}" should have {number:d} entries')
@then('journal "{journal_name}" should have {number:d} entry') @then('journal "{journal_name}" should have {number:d} entry')
def check_journal_entries(context, number, journal_name="default"): def check_journal_entries(context, number, journal_name="default"):
journal = open_journal(journal_name) journal = open_journal(context, journal_name)
assert len(journal.entries) == number assert len(journal.entries) == number
@when("the journal directory is listed") @when("the journal directory is listed")
def list_journal_directory(context, journal="default"): def list_journal_directory(context, journal="default"):
with open(install.CONFIG_FILE_PATH) as config_file: with open(context.config_path) as config_file:
config = yaml.load(config_file, Loader=yaml.FullLoader) configuration = yaml.load(config_file, Loader=yaml.FullLoader)
journal_path = config["journals"][journal] journal_path = configuration["journals"][journal]
for root, dirnames, f in os.walk(journal_path): for root, dirnames, f in os.walk(journal_path):
for file in f: for file in f:
print(os.path.join(root, file)) print(os.path.join(root, file))

View file

@ -176,7 +176,9 @@ def get_keychain(journal_name):
try: try:
return keyring.get_password("jrnl", journal_name) return keyring.get_password("jrnl", journal_name)
except RuntimeError: except keyring.errors.KeyringError as e:
if not isinstance(e, keyring.errors.NoKeyringError):
print("Failed to retrieve keyring", file=sys.stderr)
return "" return ""
@ -186,13 +188,16 @@ def set_keychain(journal_name, password):
if password is None: if password is None:
try: try:
keyring.delete_password("jrnl", journal_name) keyring.delete_password("jrnl", journal_name)
except keyring.errors.PasswordDeleteError: except keyring.errors.KeyringError:
pass pass
else: else:
try: try:
keyring.set_password("jrnl", journal_name, password) keyring.set_password("jrnl", journal_name, password)
except keyring.errors.NoKeyringError: except keyring.errors.KeyringError as e:
print( if isinstance(e, keyring.errors.NoKeyringError):
"Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/", print(
file=sys.stderr, "Keyring backend not found. Please install one of the supported backends by visiting: https://pypi.org/project/keyring/",
) file=sys.stderr,
)
else:
print("Failed to retrieve keyring", file=sys.stderr)

View file

@ -7,6 +7,7 @@ import sys
from .jrnl import run from .jrnl import run
from .args import parse_args from .args import parse_args
from .exception import JrnlError
def configure_logger(debug=False): def configure_logger(debug=False):
@ -33,5 +34,9 @@ def cli(manual_args=None):
return run(args) return run(args)
except JrnlError as e:
print(e.message, file=sys.stderr)
return 1
except KeyboardInterrupt: except KeyboardInterrupt:
return 1 return 1

View file

@ -1,13 +1,77 @@
import logging import logging
import os
import sys import sys
import colorama import colorama
import yaml import yaml
import xdg.BaseDirectory
from . import __version__
from .exception import JrnlError
from .color import ERROR_COLOR from .color import ERROR_COLOR
from .color import RESET_COLOR from .color import RESET_COLOR
from .output import list_journals from .output import list_journals
# Constants
DEFAULT_CONFIG_NAME = "jrnl.yaml"
XDG_RESOURCE = "jrnl"
DEFAULT_JOURNAL_NAME = "journal.txt"
DEFAULT_JOURNAL_KEY = "default"
def save_config(config):
config["version"] = __version__
with open(get_config_path(), "w") as f:
yaml.safe_dump(
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
)
def get_config_path():
try:
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError:
raise JrnlError(
"ConfigDirectoryIsFile",
config_directory_path=os.path.join(
xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
),
)
return os.path.join(
config_directory_path or os.path.expanduser("~"), DEFAULT_CONFIG_NAME
)
def get_default_config():
return {
"version": __version__,
"journals": {"default": get_default_journal_path()},
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
"encrypt": False,
"template": False,
"default_hour": 9,
"default_minute": 0,
"timeformat": "%Y-%m-%d %H:%M",
"tagsymbols": "@",
"highlight": True,
"linewrap": 79,
"indent_character": "|",
"colors": {
"date": "none",
"title": "none",
"body": "none",
"tags": "none",
},
}
def get_default_journal_path():
journal_data_path = xdg.BaseDirectory.save_data_path(
XDG_RESOURCE
) or os.path.expanduser("~")
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
def scope_config(config, journal_name): def scope_config(config, journal_name):
if journal_name not in config["journals"]: if journal_name not in config["journals"]:
@ -73,13 +137,11 @@ def update_config(config, new_config, scope, force_local=False):
def get_journal_name(args, config): def get_journal_name(args, config):
from . import install args.journal_name = DEFAULT_JOURNAL_KEY
args.journal_name = install.DEFAULT_JOURNAL_KEY
if args.text and args.text[0] in config["journals"]: if args.text and args.text[0] in config["journals"]:
args.journal_name = args.text[0] args.journal_name = args.text[0]
args.text = args.text[1:] args.text = args.text[1:]
elif install.DEFAULT_JOURNAL_KEY not in config["journals"]: elif DEFAULT_JOURNAL_KEY not in config["journals"]:
print("No default journal configured.", file=sys.stderr) print("No default journal configured.", file=sys.stderr)
print(list_journals(config), file=sys.stderr) print(list_journals(config), file=sys.stderr)
sys.exit(1) sys.exit(1)

View file

@ -5,6 +5,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import textwrap import textwrap
from pathlib import Path
from .color import ERROR_COLOR from .color import ERROR_COLOR
from .color import RESET_COLOR from .color import RESET_COLOR
@ -12,7 +13,11 @@ from .os_compat import on_windows
def get_text_from_editor(config, template=""): def get_text_from_editor(config, template=""):
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt") suffix = ".jrnl"
if config["template"]:
template_filename = Path(config["template"]).name
suffix = "-" + template_filename
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=suffix)
os.close(filehandle) os.close(filehandle)
with open(tmpfile, "w", encoding="utf-8") as f: with open(tmpfile, "w", encoding="utf-8") as f:

View file

@ -1,5 +1,6 @@
# Copyright (C) 2012-2021 jrnl contributors # Copyright (C) 2012-2021 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html # License: https://www.gnu.org/licenses/gpl-3.0.html
import textwrap
class UserAbort(Exception): class UserAbort(Exception):
@ -10,3 +11,28 @@ class UpgradeValidationException(Exception):
"""Raised when the contents of an upgraded journal do not match the old journal""" """Raised when the contents of an upgraded journal do not match the old journal"""
pass pass
class JrnlError(Exception):
"""Common exceptions raised by jrnl. """
def __init__(self, error_type, **kwargs):
self.error_type = error_type
self.message = self._get_error_message(**kwargs)
def _get_error_message(self, **kwargs):
error_messages = {
"ConfigDirectoryIsFile": textwrap.dedent(
"""
The path to your jrnl configuration directory is a file, not a directory:
{config_directory_path}
Removing this file will allow jrnl to save its configuration.
"""
)
}
return error_messages[self.error_type].format(**kwargs)
pass

View file

@ -8,86 +8,45 @@ import logging
import os import os
import sys import sys
import xdg.BaseDirectory from .config import DEFAULT_JOURNAL_KEY
import yaml from .config import get_config_path
from .config import get_default_config
from . import __version__ from .config import get_default_journal_path
from .config import load_config from .config import load_config
from .config import save_config
from .config import verify_config_colors from .config import verify_config_colors
from .exception import UserAbort from .exception import UserAbort
from .prompt import yesno from .prompt import yesno
from .upgrade import is_old_version from .upgrade import is_old_version
DEFAULT_CONFIG_NAME = "jrnl.yaml"
DEFAULT_JOURNAL_NAME = "journal.txt"
DEFAULT_JOURNAL_KEY = "default"
XDG_RESOURCE = "jrnl"
USER_HOME = os.path.expanduser("~")
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
CONFIG_FILE_PATH_FALLBACK = os.path.join(USER_HOME, ".jrnl_config")
JOURNAL_PATH = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or USER_HOME
JOURNAL_FILE_PATH = os.path.join(JOURNAL_PATH, DEFAULT_JOURNAL_NAME)
default_config = {
"version": __version__,
"journals": {"default": JOURNAL_FILE_PATH},
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
"encrypt": False,
"template": False,
"default_hour": 9,
"default_minute": 0,
"timeformat": "%Y-%m-%d %H:%M",
"tagsymbols": "@",
"highlight": True,
"linewrap": 79,
"indent_character": "|",
"colors": {
"date": "none",
"title": "none",
"body": "none",
"tags": "none",
},
}
def upgrade_config(config): def upgrade_config(config):
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly. """Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
This essentially automatically ports jrnl installations if new config parameters are introduced in later This essentially automatically ports jrnl installations if new config parameters are introduced in later
versions.""" versions."""
default_config = get_default_config()
missing_keys = set(default_config).difference(config) missing_keys = set(default_config).difference(config)
if missing_keys: if missing_keys:
for key in missing_keys: for key in missing_keys:
config[key] = default_config[key] config[key] = default_config[key]
save_config(config) save_config(config)
print( print(
f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", f"[Configuration updated to newest version at {get_config_path()}]",
file=sys.stderr, file=sys.stderr,
) )
def save_config(config):
config["version"] = __version__
with open(CONFIG_FILE_PATH, "w") as f:
yaml.safe_dump(
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
)
def load_or_install_jrnl(): def load_or_install_jrnl():
""" """
If jrnl is already installed, loads and returns a config object. If jrnl is already installed, loads and returns a config object.
Else, perform various prompts to install jrnl. Else, perform various prompts to install jrnl.
""" """
config_path = ( config_path = (
CONFIG_FILE_PATH get_config_path()
if os.path.exists(CONFIG_FILE_PATH) if os.path.exists(get_config_path())
else CONFIG_FILE_PATH_FALLBACK else os.path.join(os.path.expanduser("~"), ".jrnl_config")
) )
if os.path.exists(config_path): if os.path.exists(config_path):
logging.debug("Reading configuration from file %s", config_path) logging.debug("Reading configuration from file %s", config_path)
config = load_config(config_path) config = load_config(config_path)
@ -128,8 +87,10 @@ def install():
_initialize_autocomplete() _initialize_autocomplete()
# Where to create the journal? # Where to create the journal?
path_query = f"Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): " default_journal_path = get_default_journal_path()
journal_path = os.path.abspath(input(path_query).strip() or JOURNAL_FILE_PATH) path_query = f"Path to your journal file (leave blank for {default_journal_path}): "
journal_path = os.path.abspath(input(path_query).strip() or default_journal_path)
default_config = get_default_config()
default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser( default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser(
os.path.expandvars(journal_path) os.path.expandvars(journal_path)
) )

View file

@ -11,6 +11,7 @@ from .color import ERROR_COLOR
from .color import RESET_COLOR from .color import RESET_COLOR
from .config import get_journal_name from .config import get_journal_name
from .config import scope_config from .config import scope_config
from .config import get_config_path
from .editor import get_text_from_editor 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
@ -228,7 +229,7 @@ def _edit_search_results(config, journal, old_entries, **kwargs):
f""" f"""
[{ERROR_COLOR}ERROR{RESET_COLOR}: There is no editor configured.] [{ERROR_COLOR}ERROR{RESET_COLOR}: There is no editor configured.]
Please specify an editor in config file ({install.CONFIG_FILE_PATH}) Please specify an editor in config file ({get_config_path()})
to use the --edit option. to use the --edit option.
""", """,
file=sys.stderr, file=sys.stderr,

View file

@ -23,13 +23,13 @@ def deprecated_cmd(old_cmd, new_cmd, callback=None, **kwargs):
callback(**kwargs) callback(**kwargs)
def list_journals(config): def list_journals(configuration):
from . import install from . import config
"""List the journals specified in the configuration file""" """List the journals specified in the configuration file"""
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n" result = f"Journals defined in {config.get_config_path()}\n"
ml = min(max(len(k) for k in config["journals"]), 20) ml = min(max(len(k) for k in configuration["journals"]), 20)
for journal, cfg in config["journals"].items(): for journal, cfg in configuration["journals"].items():
result += " * {:{}} -> {}\n".format( result += " * {:{}} -> {}\n".format(
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
) )

8
poetry.lock generated
View file

@ -212,7 +212,7 @@ python-versions = ">=3.6"
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "21.7.0" version = "21.8.0"
description = "Store and access your passwords safely." description = "Store and access your passwords safely."
category = "main" category = "main"
optional = false optional = false
@ -225,7 +225,7 @@ pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "pytest-black (>=0.3.7)", "pytest-mypy"] testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "pytest-black (>=0.3.7)", "pytest-mypy"]
[[package]] [[package]]
@ -735,8 +735,8 @@ joblib = [
{file = "joblib-0.17.0.tar.gz", hash = "sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5"}, {file = "joblib-0.17.0.tar.gz", hash = "sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5"},
] ]
keyring = [ keyring = [
{file = "keyring-21.7.0-py3-none-any.whl", hash = "sha256:4c41ce4f6d1ee91d589a346699ef5a94ba3429603ac8f700cc0097644cdd6748"}, {file = "keyring-21.8.0-py3-none-any.whl", hash = "sha256:4be9cbaaaf83e61d6399f733d113ede7d1c73bc75cb6aeb64eee0f6ac39b30ea"},
{file = "keyring-21.7.0.tar.gz", hash = "sha256:a144f7e1044c897c3976202af868cb0ac860f4d433d5d0f8e750fa1a2f0f0b50"}, {file = "keyring-21.8.0.tar.gz", hash = "sha256:1746d3ac913d449a090caf11e9e4af00e26c3f7f7e81027872192b2398b98675"},
] ]
livereload = [ livereload = [
{file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"}, {file = "livereload-2.6.3.tar.gz", hash = "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"},

19
tests/test_exception.py Normal file
View file

@ -0,0 +1,19 @@
import textwrap
from jrnl.exception import JrnlError
def test_config_directory_exception_message():
ex = JrnlError(
"ConfigDirectoryIsFile", config_directory_path="/config/directory/path"
)
assert ex.message == textwrap.dedent(
"""
The path to your jrnl configuration directory is a file, not a directory:
/config/directory/path
Removing this file will allow jrnl to save its configuration.
"""
)