From 161d9c72623e8f6f4b8e691c2e703e2a31c95c97 Mon Sep 17 00:00:00 2001 From: Ciaran Concannon Date: Thu, 12 Jan 2023 13:56:01 +0000 Subject: [PATCH] Add `-tagged` and `-not -tagged` functionality --- jrnl/args.py | 54 +++++++++++++++++++++++++------ jrnl/controller.py | 6 ++++ jrnl/journals/Journal.py | 7 ++-- tests/bdd/features/search.feature | 30 +++++++++++++++++ tests/unit/test_parse_args.py | 2 ++ 5 files changed, 87 insertions(+), 12 deletions(-) diff --git a/jrnl/args.py b/jrnl/args.py index cf162c1a..b1055da3 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -27,11 +27,44 @@ class WrappingFormatter(argparse.RawTextHelpFormatter): text = [item for sublist in text for item in sublist] return text + class IgnoreNoneAppendAction(argparse._AppendAction): + """ + Pass -not without a following string and avoid appending + a None value to the excluded list + """ + def __call__(self, parser, namespace, values, option_string=None): if values is not None: super().__call__(parser, namespace, values, option_string) + +def parse_not_arg( + args: list[str], parsed_args: argparse.Namespace, parser: argparse.ArgumentParser +) -> argparse.Namespace: + """ + It's possible to use -not as a precursor to -starred and -tagged + to reverse their behaviour, however this requires some extra logic + to parse, and to ensure we still do not allow passing an empty -not + """ + + parsed_args.exclude_starred = False + parsed_args.exclude_tagged = False + + if "-not-starred" in "".join(args): + parsed_args.starred = False + parsed_args.exclude_starred = True + if "-not-tagged" in "".join(args): + parsed_args.tagged = False + parsed_args.exclude_tagged = True + if "-not" in args and not any( + [parsed_args.exclude_starred, parsed_args.exclude_tagged, parsed_args.excluded] + ): + parser.error("argument -not: expected 1 argument") + + return parsed_args + + def parse_args(args: list[str] = []) -> argparse.Namespace: """ Argument parsing that is doable before the config is available. @@ -241,6 +274,12 @@ def parse_args(args: list[str] = []) -> argparse.Namespace: action="store_true", help="Show only starred entries (marked with *)", ) + reading.add_argument( + "-tagged", + dest="tagged", + action="store_true", + help="Show only entries that have at least one tag", + ) reading.add_argument( "-n", dest="limit", @@ -257,8 +296,10 @@ def parse_args(args: list[str] = []) -> argparse.Namespace: default=[], metavar="TAG/FLAG", action=IgnoreNoneAppendAction, - help=("If passed a string, will exclude entries with that tag. " - "If it preceeds -starred flag, will exclude starred entries" + help=( + "If passed a string, will exclude entries with that tag. " + "Can be also used before -starred or -tagged flags, to exclude " + "starred or tagged entries respectively." ), ) @@ -395,13 +436,6 @@ def parse_args(args: list[str] = []) -> argparse.Namespace: num = re.compile(r"^-(\d+)$") args = [num.sub(r"-n \1", arg) for arg in args] parsed_args = parser.parse_intermixed_args(args) - parsed_args.exclude_starred = False - - # Handle -not where it is reversing the behaviour of a flag - if "-not-starred" in "".join(args): - parsed_args.starred = False - parsed_args.exclude_starred = True - if "-not" in args and not any([parsed_args.exclude_starred, parsed_args.excluded]): - return parser.error("argument -not: expected 1 argument") + parsed_args = parse_not_arg(args, parsed_args, parser) return parsed_args diff --git a/jrnl/controller.py b/jrnl/controller.py index 242dfbee..7a0a4965 100644 --- a/jrnl/controller.py +++ b/jrnl/controller.py @@ -90,6 +90,7 @@ def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: args.change_time, args.excluded, args.exclude_starred, + args.exclude_tagged, args.export, args.end_date, args.today_in_history, @@ -102,6 +103,7 @@ def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: args.starred, args.start_date, args.strict, + args.tagged, args.tags, ) ) @@ -271,8 +273,10 @@ def _has_search_args(args: "Namespace") -> bool: args.end_date, args.strict, args.starred, + args.tagged, args.excluded, args.exclude_starred, + args.exclude_tagged, args.contains, args.limit, ) @@ -298,8 +302,10 @@ def _filter_journal_entries(args: "Namespace", journal: Journal, **kwargs) -> No end_date=args.end_date, strict=args.strict, starred=args.starred, + tagged=args.tagged, exclude=args.excluded, exclude_starred=args.exclude_starred, + exclude_tagged=args.exclude_tagged, contains=args.contains, ) journal.limit(args.limit) diff --git a/jrnl/journals/Journal.py b/jrnl/journals/Journal.py index a3bbcf8b..994e0098 100644 --- a/jrnl/journals/Journal.py +++ b/jrnl/journals/Journal.py @@ -236,7 +236,9 @@ class Journal: start_date=None, end_date=None, starred=False, + tagged=False, exclude_starred=False, + exclude_tagged=False, strict=False, contains=None, exclude=[], @@ -260,7 +262,7 @@ class Journal: start_date = time.parse(start_date) # If strict mode is on, all tags have to be present in entry - tagged = self.search_tags.issubset if strict else self.search_tags.intersection + has_tags = self.search_tags.issubset if strict else self.search_tags.intersection def excluded(tags): return 0 < len([tag for tag in tags if tag in excluded_tags]) @@ -276,8 +278,9 @@ class Journal: result = [ entry for entry in self.entries - if (not tags or tagged(entry.tags)) + if (not tags or has_tags(entry.tags)) and (not (starred or exclude_starred) or entry.starred == starred) + and (not (tagged or exclude_tagged) or bool(entry.tags) == tagged) and (not month or entry.date.month == compare_d.month) and (not day or entry.date.day == compare_d.day) and (not year or entry.date.year == compare_d.year) diff --git a/tests/bdd/features/search.feature b/tests/bdd/features/search.feature index 6853a726..138c0285 100644 --- a/tests/bdd/features/search.feature +++ b/tests/bdd/features/search.feature @@ -138,6 +138,36 @@ Feature: Searching in a journal | basic_folder.yaml | | basic_dayone.yaml | + Scenario: Searching for tagged entries + Given we use the config "" + And we use the password "test" if prompted + When we run "jrnl -tagged" + Then we should get no error + And the output should contain "3 entries found" + + Examples: configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + | basic_dayone.yaml | + + Scenario: Searching for untagged entries + Given we use the config "empty_folder.yaml" + When we run "jrnl Tagged entry. This one has a @tag." + Then we should get no error + When we run "jrnl Untagged entry. This one has no tag." + Then we should get no error + When we run "jrnl -not -tagged" + Then we should get no error + And the output should contain "1 entry found" + And the output should contain "This one has no tag" + + Examples: configs + | config_file | + | basic_onefile.yaml | + | basic_folder.yaml | + | basic_dayone.yaml | + Scenario Outline: Searching for dates Given we use the config "" When we run "jrnl -on 2020-08-31 --short" diff --git a/tests/unit/test_parse_args.py b/tests/unit/test_parse_args.py index 72910d5e..0b266d23 100644 --- a/tests/unit/test_parse_args.py +++ b/tests/unit/test_parse_args.py @@ -24,6 +24,7 @@ def expected_args(**kwargs): "edit": False, "end_date": None, "exclude_starred": False, + "exclude_tagged": False, "today_in_history": False, "month": None, "day": None, @@ -39,6 +40,7 @@ def expected_args(**kwargs): "starred": False, "start_date": None, "strict": False, + "tagged": False, "tags": False, "text": [], "config_override": [],