Add --config-file argument to use alternate config file at runtime (#1290)

* added new CLI argument option --config-file

* pass argument and fetch alt config file if specified

* argparse argument setting update

* argument alias --cf added

* documentation update - usage of CLI argument

* fixed name-clash + unit tests

* feature test added

* #1170-alternate-config-file: Auto stash before rebase of "refs/heads/#1170-alternate-config-file"

* Update docs/advanced.md

Co-authored-by: Jonathan Wren <jonathan@nowandwren.com>

* BDD tests added

* Begin migrating/rewording --cf tests in pytest-bdd.

Uses current directory instead of deep directory structure, but requires a given for each config file referenced

* Fix issue where specifying a config-file that needs to be upgraded ended up upgrading the user config file instead

* Uncomment and rework remaining tests for pytest-bdd instead of behave

* Fix copytree for Python 3.7 (which doesn't support dirs_exist_ok)

* Minor fixes to alternative config examples

* Remove behave tests (behave is no longer in use)

* Move config file unit test to unit test dir and use pytext path fixture instead of current directory to find test data

* Use explicit "given the config exists" for copying config files instead of shoehorning in "given we use the config" twice

* Change when/when to when/and

* Clarify scenarios and fix indentation

* Confirm primary config file isn't modified when encrypting/decrypting a journal in an alternate config file

* Remove try/except on copytree since I'm no longer using the same

Co-authored-by: Jonathan Wren <jonathan@nowandwren.com>
Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
samuelgregorovic 2021-11-06 22:12:34 +01:00 committed by GitHub
parent 5057c290c1
commit ae009099ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 15 deletions

View file

@ -85,6 +85,25 @@ jrnl --config-override display_format fancy --config-override linewrap 20 \
``` ```
### Using an alternate config
You can specify an alternate configuration file for the current instance of `jrnl` using `--config-file CONFIG_FILE_PATH` where
`CONFIG_FILE_PATH` is a path to an alternate `jrnl` configuration file.
#### Examples:
```
# Use personalised configuration file for personal journal entries
jrnl --config-file ~/foo/jrnl/personal-config.yaml
# Use alternate configuration file for work-related entries
jrnl --config-file ~/foo/jrnl/work-config.yaml
# Use default configuration file (created on installation)
jrnl
```
## Multiple journal files ## Multiple journal files
You can configure `jrnl`to use with multiple journals (eg. You can configure `jrnl`to use with multiple journals (eg.

View file

@ -337,6 +337,31 @@ def parse_args(args=[]):
""", """,
) )
alternate_config = parser.add_argument_group(
"Specifies alternate config to be used",
textwrap.dedent("Applies alternate config for current session"),
)
alternate_config.add_argument(
"--config-file",
dest="config_file_path",
type=str,
default="",
help="""
Overrides default (created when first installed) config file for this command only.
Examples: \n
\t - Use a work config file for this jrnl entry, call: \n
\t jrnl --config-file /home/user1/work_config.yaml
\t - Use a personal config file stored on a thumb drive: \n
\t jrnl --config-file /media/user1/my-thumb-drive/personal_config.yaml
""",
)
alternate_config.add_argument(
"--cf", dest="config_file_path", type=str, default="", help=argparse.SUPPRESS
)
# Handle '-123' as a shortcut for '-n 123' # Handle '-123' as a shortcut for '-n 123'
num = re.compile(r"^-(\d+)$") num = re.compile(r"^-(\d+)$")
args = [num.sub(r"-n \1", arg) for arg in args] args = [num.sub(r"-n \1", arg) for arg in args]

View file

@ -47,9 +47,14 @@ def make_yaml_valid_dict(input: list) -> dict:
return runtime_modifications return runtime_modifications
def save_config(config): def save_config(config, alt_config_path=None):
"""Supply alt_config_path if using an alternate config through --config-file."""
config["version"] = __version__ config["version"] = __version__
with open(get_config_path(), "w", encoding=YAML_FILE_ENCODING) as f: with open(
alt_config_path if alt_config_path else get_config_path(),
"w",
encoding=YAML_FILE_ENCODING,
) as f:
yaml.safe_dump( yaml.safe_dump(
config, config,
f, f,

View file

@ -19,32 +19,53 @@ from .prompt import yesno
from .upgrade import is_old_version from .upgrade import is_old_version
def upgrade_config(config): def upgrade_config(config_data, alt_config_path=None):
"""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.
Supply alt_config_path if using an alternate config through --config-file."""
default_config = get_default_config() default_config = get_default_config()
missing_keys = set(default_config).difference(config) missing_keys = set(default_config).difference(config_data)
if missing_keys: if missing_keys:
for key in missing_keys: for key in missing_keys:
config[key] = default_config[key] config_data[key] = default_config[key]
save_config(config) save_config(config_data, alt_config_path)
config_path = alt_config_path if alt_config_path else get_config_path()
print( print(
f"[Configuration updated to newest version at {get_config_path()}]", f"[Configuration updated to newest version at {config_path}]",
file=sys.stderr, file=sys.stderr,
) )
def load_or_install_jrnl(): def find_default_config():
"""
If jrnl is already installed, loads and returns a config object.
Else, perform various prompts to install jrnl.
"""
config_path = ( config_path = (
get_config_path() get_config_path()
if os.path.exists(get_config_path()) if os.path.exists(get_config_path())
else os.path.join(os.path.expanduser("~"), ".jrnl_config") else os.path.join(os.path.expanduser("~"), ".jrnl_config")
) )
return config_path
def find_alt_config(alt_config):
if os.path.exists(alt_config):
return alt_config
else:
print(
"Alternate configuration file not found at path specified.", file=sys.stderr
)
print("Exiting.", file=sys.stderr)
sys.exit(1)
def load_or_install_jrnl(alt_config_path):
"""
If jrnl is already installed, loads and returns a default config object.
If alternate config is specified via --config-file flag, it will be used.
Else, perform various prompts to install jrnl.
"""
config_path = (
find_alt_config(alt_config_path) if alt_config_path else find_default_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)
@ -68,7 +89,7 @@ def load_or_install_jrnl():
print("Exiting.", file=sys.stderr) print("Exiting.", file=sys.stderr)
sys.exit(1) sys.exit(1)
upgrade_config(config) upgrade_config(config, alt_config_path)
verify_config_colors(config) verify_config_colors(config)
else: else:

View file

@ -36,7 +36,7 @@ def run(args):
# Load the config, and extract journal name # Load the config, and extract journal name
try: try:
config = install.load_or_install_jrnl() config = install.load_or_install_jrnl(args.config_file_path)
original_config = config.copy() original_config = config.copy()
# Apply config overrides # Apply config overrides

View file

@ -0,0 +1,92 @@
Feature: Multiple journals
Scenario: Read a journal from an alternate config
Given the config "basic_onefile.yaml" exists
And we use the config "multiple.yaml"
When we run "jrnl --cf basic_onefile.yaml -999"
Then the output should not contain "My first entry" # from multiple.yaml
And the output should contain "Lorem ipsum" # from basic_onefile.yaml
Scenario: Write to default journal by default using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl --cf multiple.yaml this goes to default"
And we run "jrnl -1"
Then the output should not contain "this goes to default"
When we run "jrnl --cf multiple.yaml -1"
Then the output should contain "this goes to default"
Scenario: Write to specified journal using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl work --cf multiple.yaml a long day in the office"
And we run "jrnl default --cf multiple.yaml -1"
Then the output should contain "But I'm better"
When we run "jrnl work --cf multiple.yaml -1"
Then the output should contain "a long day in the office"
Scenario: Tell user which journal was used while using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl --cf multiple.yaml work a long day in the office"
Then we should see the message "Entry added to work journal"
Scenario: Write to specified journal with a timestamp using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl work --cf multiple.yaml 23 july 2012: a long day in the office"
And we run "jrnl --cf multiple.yaml -1"
Then the output should contain "But I'm better"
When we run "jrnl --cf multiple.yaml work -1"
Then the output should contain "a long day in the office"
And the output should contain "2012-07-23"
Scenario: Write to specified journal without a timestamp but with colon using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl work --cf multiple.yaml : a long day in the office"
And we run "jrnl --cf multiple.yaml -1"
Then the output should contain "But I'm better"
When we run "jrnl --cf multiple.yaml work -1"
Then the output should contain "a long day in the office"
Scenario: Create new journals as required using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl ideas -1"
Then the output should be empty
When we run "jrnl ideas --cf multiple.yaml 23 july 2012: sell my junk on ebay and make lots of money"
Then the output should contain "Journal 'ideas' created"
When we run "jrnl ideas --cf multiple.yaml -1"
Then the output should contain "sell my junk on ebay and make lots of money"
Scenario: Don't crash if no default journal is specified using an alternate config
Given the config "bug343.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl --cf bug343.yaml a long day in the office"
Then we should see the message "No default journal configured"
Scenario: Don't crash if no file exists for a configured encrypted journal using an alternate config
Given the config "multiple.yaml" exists
And we use the config "basic_onefile.yaml"
When we run "jrnl new_encrypted --cf multiple.yaml Adding first entry" and enter
these three eyes
these three eyes
n
Then we should see the message "Encrypted journal 'new_encrypted' created"
Scenario: Don't overwrite main config when encrypting a journal in an alternate config
Given the config "basic_onefile.yaml" exists
And we use the config "multiple.yaml"
When we run "jrnl --cf basic_onefile.yaml --encrypt" and enter
these three eyes
these three eyes
n
Then we should see the message "Journal encrypted to features/journals/basic_onefile.journal"
And the config should contain "encrypt: false" # multiple.yaml remains unchanged
Scenario: Don't overwrite main config when decrypting a journal in an alternate config
Given the config "editor_encrypted.yaml" exists
And we use the config "basic_encrypted.yaml"
When we run "jrnl --cf editor_encrypted.yaml --decrypt"
Then the config should contain "encrypt: true" # basic_encrypted remains unchanged

View file

@ -1,6 +1,7 @@
from pytest_bdd import scenarios from pytest_bdd import scenarios
scenarios("features/build.feature") scenarios("features/build.feature")
scenarios("features/config_file.feature")
scenarios("features/core.feature") scenarios("features/core.feature")
scenarios("features/datetime.feature") scenarios("features/datetime.feature")
scenarios("features/delete.feature") scenarios("features/delete.feature")

View file

@ -105,6 +105,14 @@ def we_use_the_config(config_file, temp_dir, working_dir):
return config_dest return config_dest
@given(parse('the config "{config_file}" exists'), target_fixture="config_path")
@given('the config "<config_file>" exists', target_fixture="config_path")
def config_exists(config_file, temp_dir, working_dir):
config_source = os.path.join(working_dir, "data", "configs", config_file)
config_dest = os.path.join(temp_dir.name, config_file)
shutil.copy2(config_source, config_dest)
@given(parse('we use the password "{pw}" if prompted'), target_fixture="password") @given(parse('we use the password "{pw}" if prompted'), target_fixture="password")
def use_password_forever(pw): def use_password_forever(pw):
return pw return pw

View file

@ -0,0 +1,22 @@
import pytest
import os
from jrnl.install import find_alt_config
def test_find_alt_config(request):
work_config_path = os.path.join(
request.fspath.dirname, "..", "data", "configs", "basic_onefile.yaml"
)
found_alt_config = find_alt_config(work_config_path)
assert found_alt_config == work_config_path
def test_find_alt_config_not_exist(request):
bad_config_path = os.path.join(
request.fspath.dirname, "..", "data", "configs", "not-existing-config.yaml"
)
with pytest.raises(SystemExit) as ex:
found_alt_config = find_alt_config(bad_config_path)
assert found_alt_config is not None
assert isinstance(ex.value, SystemExit)

View file

@ -37,6 +37,7 @@ def expected_args(**kwargs):
"tags": False, "tags": False,
"text": [], "text": [],
"config_override": [], "config_override": [],
"config_file_path": "",
} }
return {**default_args, **kwargs} return {**default_args, **kwargs}