Move standalone commands into their own file

Also, cleaned up the way the arg parser handles standalone commands.
Rather than checking individually for each command, you can now register
the command in the proper place, and it will be run with all known
arguments (and cofig if available).
This commit is contained in:
Jonathan Wren 2020-07-01 18:25:27 -07:00
parent 0bc5f9d453
commit c98d01bb8b
4 changed files with 302 additions and 216 deletions

View file

@ -19,16 +19,14 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import argparse
import logging
import packaging.version
import platform
import re
import sys
import jrnl
from . import install, plugins, util
from .commands import list_journals
from .parsing import parse_args_before_config
from .Journal import PlainJournal, open_journal
from .util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR, UserAbort
@ -36,186 +34,6 @@ log = logging.getLogger(__name__)
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
def parse_args(args=None):
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
"--version",
dest="version",
action="store_true",
help="prints version information and exits",
)
parser.add_argument(
"--diagnostic",
dest="diagnostic",
action="store_true",
help="outputs diagnostic information and exits",
)
parser.add_argument(
"-ls", dest="ls", action="store_true", help="displays accessible journals"
)
parser.add_argument(
"-d", "--debug", dest="debug", action="store_true", help="execute in debug mode"
)
composing = parser.add_argument_group(
"Composing",
'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."',
)
reading = parser.add_argument_group(
"Reading",
"Specifying either of these parameters will display posts of your journal",
)
reading.add_argument(
"-from", dest="start_date", metavar="DATE", help="View entries after this date"
)
reading.add_argument(
"-until",
"-to",
dest="end_date",
metavar="DATE",
help="View entries before this date",
)
reading.add_argument(
"-contains", dest="contains", help="View entries containing a specific string"
)
reading.add_argument(
"-on", dest="on_date", metavar="DATE", help="View entries on this date"
)
reading.add_argument(
"-and",
dest="strict",
action="store_true",
help="Filter by tags using AND (default: OR)",
)
reading.add_argument(
"-starred",
dest="starred",
action="store_true",
help="Show only starred entries",
)
reading.add_argument(
"-n",
dest="limit",
default=None,
metavar="N",
help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.",
nargs="?",
type=int,
)
reading.add_argument(
"-not",
dest="excluded",
nargs="?",
default=[],
metavar="E",
action="append",
help="Exclude entries with these tags",
)
exporting = parser.add_argument_group(
"Export / Import", "Options for transmogrifying your journal"
)
exporting.add_argument(
"-s",
"--short",
dest="short",
action="store_true",
help="Show only titles or line containing the search tags",
)
exporting.add_argument(
"--tags",
dest="tags",
action="store_true",
help="Returns a list of all tags and number of occurences",
)
exporting.add_argument(
"--export",
metavar="TYPE",
dest="export",
choices=plugins.EXPORT_FORMATS,
help="Export your journal. TYPE can be {}.".format(
plugins.util.oxford_list(plugins.EXPORT_FORMATS)
),
default=False,
const=None,
)
exporting.add_argument(
"-o",
metavar="OUTPUT",
dest="output",
help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.",
default=False,
const=None,
)
exporting.add_argument(
"--import",
metavar="TYPE",
dest="import_",
choices=plugins.IMPORT_FORMATS,
help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format(
plugins.util.oxford_list(plugins.IMPORT_FORMATS)
),
default=False,
const="jrnl",
nargs="?",
)
exporting.add_argument(
"-i",
metavar="INPUT",
dest="input",
help="Optionally specifies input file when using --import.",
default=False,
const=None,
)
exporting.add_argument(
"--encrypt",
metavar="FILENAME",
dest="encrypt",
help="Encrypts your existing journal with a new password",
nargs="?",
default=False,
const=None,
)
exporting.add_argument(
"--decrypt",
metavar="FILENAME",
dest="decrypt",
help="Decrypts your journal and stores it in plain text",
nargs="?",
default=False,
const=None,
)
exporting.add_argument(
"--edit",
dest="edit",
help="Opens your editor to edit the selected entries.",
action="store_true",
)
exporting.add_argument(
"--delete",
dest="delete",
action="store_true",
help="Opens an interactive interface for deleting entries.",
)
# Everything else
composing.add_argument("text", metavar="", nargs="*")
if not args:
args = []
# Handle '-123' as a shortcut for '-n 123'
num = re.compile(r"^-(\d+)$")
args = [num.sub(r"-n \1", arg) for arg in args]
return parser.parse_intermixed_args(args)
def guess_mode(args, config):
"""Guesses the mode (compose, read or export) from the given arguments"""
compose = True
@ -282,17 +100,6 @@ def decrypt(journal, filename=None):
)
def list_journals(config):
"""List the journals specified in the configuration file"""
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
ml = min(max(len(k) for k in config["journals"]), 20)
for journal, cfg in config["journals"].items():
result += " * {:{}} -> {}\n".format(
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
)
return result
def update_config(config, new_config, scope, force_local=False):
"""Updates a config dict with new values - either global if scope is None
or config['journals'][scope] is just a string pointing to a journal file,
@ -332,30 +139,29 @@ Python 3.7 (or higher) soon.
if manual_args is None:
manual_args = sys.argv[1:]
args = parse_args(manual_args)
args = parse_args_before_config(manual_args)
# import pprint
# pp = pprint.PrettyPrinter(depth=4)
# pp.pprint(args)
configure_logger(args.debug)
if args.version:
version_str = f"{jrnl.__title__} version {jrnl.__version__}"
print(version_str)
sys.exit(0)
if args.diagnostic:
print(
f"jrnl: {jrnl.__version__}\n"
f"Python: {sys.version}\n"
f"OS: {platform.system()} {platform.release()}"
)
# Run command if possible before config is available
if args.preconfig_cmd is not None:
args.preconfig_cmd(args)
sys.exit(0)
# Load the config
try:
config = install.load_or_install_jrnl()
except UserAbort as err:
print(f"\n{err}", file=sys.stderr)
sys.exit(1)
if args.ls:
print(list_journals(config))
# Run command now that config is available
if args.postconfig_cmd is not None:
args.postconfig_cmd(config=config, args=args)
sys.exit(0)
log.debug('Using configuration "%s"', config)

52
jrnl/commands.py Normal file
View file

@ -0,0 +1,52 @@
def deprecated_cmd(old_cmd, new_cmd, callback, **kwargs):
import sys
from .util import RESET_COLOR, WARNING_COLOR
print(
WARNING_COLOR,
f"\nThe command {old_cmd} is deprecated and will be removed from jrnl soon.\n"
f"Please use {new_cmd} instead.\n",
RESET_COLOR,
file=sys.stderr,
)
callback(**kwargs)
def preconfig_diagnostic(_):
import platform
import sys
import jrnl
print(
f"jrnl: {jrnl.__version__}\n"
f"Python: {sys.version}\n"
f"OS: {platform.system()} {platform.release()}"
)
def preconfig_version(_):
import jrnl
version_str = f"{jrnl.__title__} version {jrnl.__version__}"
print(version_str)
def preconfig_command(args):
print("this is a pre-config command")
def postconfig_list(config, **kwargs):
print(list_journals(config))
def list_journals(config):
from . import install
"""List the journals specified in the configuration file"""
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
ml = min(max(len(k) for k in config["journals"]), 20)
for journal, cfg in config["journals"].items():
result += " * {:{}} -> {}\n".format(
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
)
return result

221
jrnl/parsing.py Normal file
View file

@ -0,0 +1,221 @@
import argparse
import re
from . import plugins
from .commands import preconfig_version
from .commands import preconfig_diagnostic
from .commands import postconfig_list
from .commands import deprecated_cmd
def parse_args_before_config(args=None):
"""
Argument parsing that is doable before the config is available.
Everything else goes into "text" for later parsing.
"""
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
"--version",
action="store_const",
const=preconfig_version,
dest="preconfig_cmd",
help="prints version information and exits",
)
parser.add_argument(
"--cmd1",
action="store_const",
const=lambda: print("cmd1"),
dest="preconfig_cmd",
)
parser.add_argument(
"--diagnostic",
action="store_const",
const=preconfig_diagnostic,
dest="preconfig_cmd",
help="outputs diagnostic information and exits",
)
parser.add_argument(
"--ls",
"--list",
action="store_const",
const=postconfig_list,
dest="postconfig_cmd",
help="lists all configured journals",
)
parser.add_argument(
"-ls",
action="store_const",
const=lambda **kwargs: deprecated_cmd(
"-ls", "--ls or --list", callback=postconfig_list, **kwargs
),
dest="postconfig_cmd",
help="displays accessible journals",
)
parser.add_argument(
"-d", "--debug", dest="debug", action="store_true", help="execute in debug mode"
)
composing = parser.add_argument_group(
"Composing",
'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."',
)
reading = parser.add_argument_group(
"Reading",
"Specifying either of these parameters will display posts of your journal",
)
reading.add_argument(
"-from", dest="start_date", metavar="DATE", help="View entries after this date"
)
reading.add_argument(
"-until",
"-to",
dest="end_date",
metavar="DATE",
help="View entries before this date",
)
reading.add_argument(
"-contains", dest="contains", help="View entries containing a specific string"
)
reading.add_argument(
"-on", dest="on_date", metavar="DATE", help="View entries on this date"
)
reading.add_argument(
"-and",
dest="strict",
action="store_true",
help="Filter by tags using AND (default: OR)",
)
reading.add_argument(
"-starred",
dest="starred",
action="store_true",
help="Show only starred entries",
)
reading.add_argument(
"-n",
dest="limit",
default=None,
metavar="N",
help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.",
nargs="?",
type=int,
)
reading.add_argument(
"-not",
dest="excluded",
nargs="?",
default=[],
metavar="E",
action="append",
help="Exclude entries with these tags",
)
exporting = parser.add_argument_group(
"Export / Import", "Options for transmogrifying your journal"
)
exporting.add_argument(
"-s",
"--short",
dest="short",
action="store_true",
help="Show only titles or line containing the search tags",
)
exporting.add_argument(
"--tags",
dest="tags",
action="store_true",
help="Returns a list of all tags and number of occurences",
)
exporting.add_argument(
"--export",
metavar="TYPE",
dest="export",
choices=plugins.EXPORT_FORMATS,
help="Export your journal. TYPE can be {}.".format(
plugins.util.oxford_list(plugins.EXPORT_FORMATS)
),
default=False,
const=None,
)
exporting.add_argument(
"-o",
metavar="OUTPUT",
dest="output",
help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.",
default=False,
const=None,
)
exporting.add_argument(
"--import",
metavar="TYPE",
dest="import_",
choices=plugins.IMPORT_FORMATS,
help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format(
plugins.util.oxford_list(plugins.IMPORT_FORMATS)
),
default=False,
const="jrnl",
nargs="?",
)
exporting.add_argument(
"-i",
metavar="INPUT",
dest="input",
help="Optionally specifies input file when using --import.",
default=False,
const=None,
)
exporting.add_argument(
"--encrypt",
metavar="FILENAME",
dest="encrypt",
help="Encrypts your existing journal with a new password",
nargs="?",
default=False,
const=None,
)
exporting.add_argument(
"--decrypt",
metavar="FILENAME",
dest="decrypt",
help="Decrypts your journal and stores it in plain text",
nargs="?",
default=False,
const=None,
)
exporting.add_argument(
"--edit",
dest="edit",
help="Opens your editor to edit the selected entries.",
action="store_true",
)
exporting.add_argument(
"--delete",
dest="delete",
action="store_true",
help="Opens an interactive interface for deleting entries.",
)
# Everything else
composing.add_argument("text", metavar="", nargs="*")
if not args:
args = []
# Handle '-123' as a shortcut for '-n 123'
num = re.compile(r"^-(\d+)$")
args = [num.sub(r"-n \1", arg) for arg in args]
# return parser.parse_args(args)
return parser.parse_intermixed_args(args)
def parse_args_after_config(args=None):
return None

View file

@ -1,4 +1,4 @@
from jrnl.cli import parse_args
from jrnl.cli import parse_args_before_config as parse_args
import pytest
import shlex
@ -16,7 +16,6 @@ def expected_args(**kwargs):
"debug": False,
"decrypt": False,
"delete": False,
"diagnostic": False,
"edit": False,
"encrypt": False,
"end_date": None,
@ -25,16 +24,16 @@ def expected_args(**kwargs):
"import_": False,
"input": False,
"limit": None,
"ls": False,
"on_date": None,
"output": False,
"preconfig_cmd": None,
"postconfig_cmd": None,
"short": False,
"starred": False,
"start_date": None,
"strict": False,
"tags": False,
"text": [],
"version": False,
}
return {**default_args, **kwargs}
@ -57,7 +56,11 @@ def test_delete_alone():
def test_diagnostic_alone():
assert cli_as_dict("--diagnostic") == expected_args(diagnostic=True)
from jrnl.commands import preconfig_diagnostic
assert cli_as_dict("--diagnostic") == expected_args(
preconfig_cmd=preconfig_diagnostic
)
def test_edit_alone():
@ -134,7 +137,9 @@ def test_limit_shorthand_alone():
def test_list_alone():
assert cli_as_dict("-ls") == expected_args(ls=True)
from jrnl.commands import postconfig_list
assert cli_as_dict("--ls") == expected_args(postconfig_cmd=postconfig_list)
def test_on_date_alone():
@ -169,7 +174,9 @@ def test_text_alone():
def test_version_alone():
assert cli_as_dict("--version") == expected_args(version=True)
from jrnl.commands import preconfig_version
assert cli_as_dict("--version") == expected_args(preconfig_cmd=preconfig_version)
# @see https://github.com/jrnl-org/jrnl/issues/520