From c98d01bb8bf493a2d9dd1f6457c2d99a0ca29d73 Mon Sep 17 00:00:00 2001 From: Jonathan Wren Date: Wed, 1 Jul 2020 18:25:27 -0700 Subject: [PATCH] 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). --- jrnl/cli.py | 224 +++------------------------------------ jrnl/commands.py | 52 +++++++++ jrnl/parsing.py | 221 ++++++++++++++++++++++++++++++++++++++ tests/test_parse_args.py | 21 ++-- 4 files changed, 302 insertions(+), 216 deletions(-) create mode 100644 jrnl/commands.py create mode 100644 jrnl/parsing.py diff --git a/jrnl/cli.py b/jrnl/cli.py index 0ca54d65..50bde3ff 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -19,16 +19,14 @@ along with this program. If not, see . """ -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) diff --git a/jrnl/commands.py b/jrnl/commands.py new file mode 100644 index 00000000..2c1f2d90 --- /dev/null +++ b/jrnl/commands.py @@ -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 diff --git a/jrnl/parsing.py b/jrnl/parsing.py new file mode 100644 index 00000000..b84ab1f3 --- /dev/null +++ b/jrnl/parsing.py @@ -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 diff --git a/tests/test_parse_args.py b/tests/test_parse_args.py index f9af643e..9df8c200 100644 --- a/tests/test_parse_args.py +++ b/tests/test_parse_args.py @@ -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