From 7bd15d12adca5a36e6a56ecbd6a53dc06c5f7f4c Mon Sep 17 00:00:00 2001 From: Ciaran Concannon Date: Sat, 28 Jan 2023 19:45:01 +0000 Subject: [PATCH 1/2] Search for entries with no tags or stars with `-not -starred` and `-not -tagged` (#1663) * Allow for `-not -starred` to search for unstarred entries * Add `-tagged` and `-not -tagged` functionality --- jrnl/args.py | 59 ++++++++++++++++++++++++++++--- jrnl/controller.py | 9 +++++ jrnl/journals/Journal.py | 30 +++++++++------- tests/bdd/features/search.feature | 44 +++++++++++++++++++++++ tests/unit/test_parse_args.py | 3 ++ 5 files changed, 127 insertions(+), 18 deletions(-) diff --git a/jrnl/args.py b/jrnl/args.py index d23e1e53..b1055da3 100644 --- a/jrnl/args.py +++ b/jrnl/args.py @@ -28,6 +28,43 @@ class WrappingFormatter(argparse.RawTextHelpFormatter): 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. @@ -237,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", @@ -249,11 +292,15 @@ def parse_args(args: list[str] = []) -> argparse.Namespace: reading.add_argument( "-not", dest="excluded", - nargs=1, + nargs="?", default=[], - metavar="TAG", - action="extend", - help="Exclude entries with this tag", + metavar="TAG/FLAG", + action=IgnoreNoneAppendAction, + 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." + ), ) search_options_msg = """ These help you do various tasks with the selected entries from your search. @@ -388,5 +435,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace: # Handle '-123' as a shortcut for '-n 123' 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 = parse_not_arg(args, parsed_args, parser) - return parser.parse_intermixed_args(args) + return parsed_args diff --git a/jrnl/controller.py b/jrnl/controller.py index 7d7c87ce..7a0a4965 100644 --- a/jrnl/controller.py +++ b/jrnl/controller.py @@ -89,6 +89,8 @@ def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: args.edit, args.change_time, args.excluded, + args.exclude_starred, + args.exclude_tagged, args.export, args.end_date, args.today_in_history, @@ -101,6 +103,7 @@ def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool: args.starred, args.start_date, args.strict, + args.tagged, args.tags, ) ) @@ -270,7 +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, ) @@ -296,7 +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 5a8b016e..994e0098 100644 --- a/jrnl/journals/Journal.py +++ b/jrnl/journals/Journal.py @@ -229,16 +229,19 @@ class Journal: def filter( self, - tags: list = [], - month: str | int | None = None, - day: str | int | None = None, - year: str | None = None, - start_date: str | None = None, - end_date: str | None = None, - starred: bool = False, - strict: bool = False, - contains: bool = None, - exclude: list = [], + tags=[], + month=None, + day=None, + year=None, + start_date=None, + end_date=None, + starred=False, + tagged=False, + exclude_starred=False, + exclude_tagged=False, + strict=False, + contains=None, + exclude=[], ): """Removes all entries from the journal that don't match the filter. @@ -259,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]) @@ -275,8 +278,9 @@ class Journal: result = [ entry for entry in self.entries - if (not tags or tagged(entry.tags)) - and (not starred or entry.starred) + 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 d653a621..138c0285 100644 --- a/tests/bdd/features/search.feature +++ b/tests/bdd/features/search.feature @@ -124,6 +124,50 @@ Feature: Searching in a journal | basic_folder.yaml | | basic_dayone.yaml | + + Scenario: Searching for unstarred entries + Given we use the config "" + And we use the password "test" if prompted + When we run "jrnl -not -starred" + Then we should get no error + And the output should contain "2 entries found" + + Examples: configs + | config_file | + | basic_onefile.yaml | + | 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 ccb7f5a2..0b266d23 100644 --- a/tests/unit/test_parse_args.py +++ b/tests/unit/test_parse_args.py @@ -23,6 +23,8 @@ def expected_args(**kwargs): "change_time": None, "edit": False, "end_date": None, + "exclude_starred": False, + "exclude_tagged": False, "today_in_history": False, "month": None, "day": None, @@ -38,6 +40,7 @@ def expected_args(**kwargs): "starred": False, "start_date": None, "strict": False, + "tagged": False, "tags": False, "text": [], "config_override": [], From d4ce2a84cfbea665e5249f06d6112dd9cc661637 Mon Sep 17 00:00:00 2001 From: Jrnl Bot Date: Sat, 28 Jan 2023 19:47:17 +0000 Subject: [PATCH 2/2] Update changelog [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dff0f843..ad10e106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ **Implemented enhancements:** - Don't import cryptography package if not needed [\#1521](https://github.com/jrnl-org/jrnl/issues/1521) +- Search for entries with no tags or stars with `-not -starred` and `-not -tagged` [\#1663](https://github.com/jrnl-org/jrnl/pull/1663) ([cjcon90](https://github.com/cjcon90)) - Refactor flow for easier access to some files \(avoid things like `jrnl.Journal.Journal` and `jrnl.jrnl` co-existing\) [\#1662](https://github.com/jrnl-org/jrnl/pull/1662) ([wren](https://github.com/wren)) - Add more type hints [\#1642](https://github.com/jrnl-org/jrnl/pull/1642) ([outa](https://github.com/outa)) - Add `rich` handler to debug logging [\#1627](https://github.com/jrnl-org/jrnl/pull/1627) ([wren](https://github.com/wren))