From 2e81f05c078bfcff837db1786d535a8cc767ec0f Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Sun, 8 Mar 2020 11:40:32 +0700 Subject: Implemented `search` as a subcommand for `tag` that will search in contents instead of names - `!tag search` will search for multiple keywords, separated by comma, and return tags that has ALL of these keywords. ` !tag search any` is the same as `!tag search` but it return tags that has ANY of the keyword instead. --- bot/cogs/tags.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 5da9a4148..965a29596 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -1,7 +1,7 @@ import logging import re import time -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from discord import Colour, Embed from discord.ext.commands import Cog, Context, group @@ -86,11 +86,60 @@ class Tags(Cog): return self._get_suggestions(tag_name) return found + async def _get_tags_via_content(self, check: callable, keywords: str) -> Optional[Embed]: + """ + Search for tags via contents. + + `predicate` will be either any or all, or a custom callable to search. Must return a bool. + """ + await self._get_tags() + + keywords_processed: Tuple[str] = tuple(query.strip().casefold() for query in keywords.split(',')) + founds: list = [ + tag + for tag in self._cache.values() + if check(query in tag['embed']['description'] for query in keywords_processed) + ] + + if not founds: + return None + elif len(founds) == 1: + return Embed().from_dict(founds[0]['embed']) + else: + return Embed( + title='Did you mean ...', + description='\n'.join(tag['title'] for tag in founds[:10]) + ) + @group(name='tags', aliases=('tag', 't'), invoke_without_command=True) async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: """Show all known tags, a single tag, or run a subcommand.""" await ctx.invoke(self.get_command, tag_name=tag_name) + @tags_group.group(name='search', invoke_without_command=True) + async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: + """ + Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. + + Only search for tags that has ALL the keywords. + """ + result = await self._get_tags_via_content(all, keywords) + if not result: + return + await ctx.send(embed=result) + + @search_tag_content.command(name='any') + async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = None) -> None: + """ + Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. + + Search for tags that has ANY of the keywords. + """ + result = await self._get_tags_via_content(any, keywords or 'any') + if not result: + return + await ctx.send(embed=result) + @tags_group.command(name='get', aliases=('show', 'g')) async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: """Get a specified tag, or a list of all tags if no tag is specified.""" -- cgit v1.2.3 From 76fccc1ea47162346d60736db638eea7166222ae Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Sun, 8 Mar 2020 12:06:10 +0700 Subject: Refactored tag searching via keywords in contents - Refactored `if` block - change to only send result when there is any result. - Added better type hinting for `check` argument of `_get_tags_via_content` - changed from `callable` to `Callable[[Iterable], bool]`. Thanks to @markkoz 's reviews Co-Authored-By: Mark --- bot/cogs/tags.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 965a29596..63b529945 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -1,7 +1,7 @@ import logging import re import time -from typing import Dict, List, Optional, Tuple +from typing import Callable, Dict, Iterable, List, Optional, Tuple from discord import Colour, Embed from discord.ext.commands import Cog, Context, group @@ -86,7 +86,7 @@ class Tags(Cog): return self._get_suggestions(tag_name) return found - async def _get_tags_via_content(self, check: callable, keywords: str) -> Optional[Embed]: + async def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> Optional[Embed]: """ Search for tags via contents. @@ -124,9 +124,8 @@ class Tags(Cog): Only search for tags that has ALL the keywords. """ result = await self._get_tags_via_content(all, keywords) - if not result: - return - await ctx.send(embed=result) + if result: + await ctx.send(embed=result) @search_tag_content.command(name='any') async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = None) -> None: @@ -136,9 +135,8 @@ class Tags(Cog): Search for tags that has ANY of the keywords. """ result = await self._get_tags_via_content(any, keywords or 'any') - if not result: - return - await ctx.send(embed=result) + if result: + await ctx.send(embed=result) @tags_group.command(name='get', aliases=('show', 'g')) async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: -- cgit v1.2.3 From 89f86f873d7cd6ade626a0a91c5d9e09c5c14102 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Sun, 8 Mar 2020 12:37:42 +0700 Subject: Fixed searching for `,` returing all tags. Made it more descriptive when multiple tags are found. - Added a truthy check for each `query` since `','.split()` returns a list of two empty strings. - Changed from `Did you mean ...` to `Here are the tags containing the given keyword(s):` to be much more descriptive about the results - they are `tag` and not `term` to be searched. --- bot/cogs/tags.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 63b529945..49ed87c92 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -94,7 +94,8 @@ class Tags(Cog): """ await self._get_tags() - keywords_processed: Tuple[str] = tuple(query.strip().casefold() for query in keywords.split(',')) + keywords_processed: Tuple[str] = tuple(query.strip().casefold() for query in keywords.split(',') if query) + keywords_processed = keywords_processed or (keywords,) founds: list = [ tag for tag in self._cache.values() @@ -106,10 +107,13 @@ class Tags(Cog): elif len(founds) == 1: return Embed().from_dict(founds[0]['embed']) else: - return Embed( - title='Did you mean ...', + is_plural: bool = len(keywords_processed) > 1 or any(kw.count(' ') for kw in keywords_processed) + embed = Embed( + title=f"Here are the tags containing the given keyword{'s' * is_plural}:", description='\n'.join(tag['title'] for tag in founds[:10]) ) + embed.set_footer(text=f"Keyword{'s' * is_plural} used: {keywords}"[:1024]) + return embed @group(name='tags', aliases=('tag', 't'), invoke_without_command=True) async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: -- cgit v1.2.3 From dd707182b4f4f4ce98353d5c82092f48dd8fb5c2 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Sun, 8 Mar 2020 21:30:18 +0700 Subject: Refactored dense codes, removed obvious type hint. - Show the process of sanitizing the List[str] `keywords_processed`. - Show the process of finding tag for `matching_tags` ( was `founds` ). - Refactored the logic to find boolean `is_plural`. - Minor wording changes for docstring. --- bot/cogs/tags.py | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 49ed87c92..89f3acb6d 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -1,7 +1,7 @@ import logging import re import time -from typing import Callable, Dict, Iterable, List, Optional, Tuple +from typing import Callable, Dict, Iterable, List, Optional from discord import Colour, Embed from discord.ext.commands import Cog, Context, group @@ -90,27 +90,37 @@ class Tags(Cog): """ Search for tags via contents. - `predicate` will be either any or all, or a custom callable to search. Must return a bool. + `predicate` will be the built-in any, all, or a custom callable. Must return a bool. """ await self._get_tags() - keywords_processed: Tuple[str] = tuple(query.strip().casefold() for query in keywords.split(',') if query) - keywords_processed = keywords_processed or (keywords,) - founds: list = [ - tag - for tag in self._cache.values() - if check(query in tag['embed']['description'] for query in keywords_processed) - ] - - if not founds: + keywords_processed: List[str] = [] + for keyword in keywords.split(','): + keyword_sanitized = keyword.strip().casefold() + if not keyword_sanitized: + # this happens when there are leading / trailing / consecutive comma. + continue + keywords_processed.append(keyword_sanitized) + + if not keywords_processed: + # after sanitizing, we can end up with an empty list, for example when keywords is ',' + # in that case, we simply want to search for such keywords directly instead. + keywords_processed = [keywords] + + matching_tags = [] + for tag in self._cache.values(): + if check(query in tag['embed']['description'].casefold() for query in keywords_processed): + matching_tags.append(tag) + + if not matching_tags: return None - elif len(founds) == 1: - return Embed().from_dict(founds[0]['embed']) + elif len(matching_tags) == 1: + return Embed().from_dict(matching_tags[0]['embed']) else: - is_plural: bool = len(keywords_processed) > 1 or any(kw.count(' ') for kw in keywords_processed) + is_plural = len(keywords_processed) > 1 or keywords.strip().count(' ') > 1 embed = Embed( title=f"Here are the tags containing the given keyword{'s' * is_plural}:", - description='\n'.join(tag['title'] for tag in founds[:10]) + description='\n'.join(tag['title'] for tag in matching_tags[:10]) ) embed.set_footer(text=f"Keyword{'s' * is_plural} used: {keywords}"[:1024]) return embed -- cgit v1.2.3 From 139a7148e1ec51ae41cff12c3d32ab6f52c95aef Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Sun, 8 Mar 2020 23:01:02 +0700 Subject: Fixed `is_plural` counting 1 less space. Co-Authored-By: Mark --- bot/cogs/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index 89f3acb6d..e3ade07a9 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -117,7 +117,7 @@ class Tags(Cog): elif len(matching_tags) == 1: return Embed().from_dict(matching_tags[0]['embed']) else: - is_plural = len(keywords_processed) > 1 or keywords.strip().count(' ') > 1 + is_plural = len(keywords_processed) > 1 or keywords.strip().count(' ') > 0 embed = Embed( title=f"Here are the tags containing the given keyword{'s' * is_plural}:", description='\n'.join(tag['title'] for tag in matching_tags[:10]) -- cgit v1.2.3 From 8bf5ce5a831e389cc7af07fc0b4853ea21ec0c71 Mon Sep 17 00:00:00 2001 From: Shirayuki Nekomata Date: Mon, 9 Mar 2020 11:33:38 +0700 Subject: Refactored to use paginator like normal `!tag` - Split `_get_tags_via_content` - introduce `_send_matching_tags` - `_send_matching_tags` will send and paginate like `!tag` - Simplified `is_plural` even more. --- bot/cogs/tags.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/bot/cogs/tags.py b/bot/cogs/tags.py index e3ade07a9..c6b442912 100644 --- a/bot/cogs/tags.py +++ b/bot/cogs/tags.py @@ -86,7 +86,7 @@ class Tags(Cog): return self._get_suggestions(tag_name) return found - async def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> Optional[Embed]: + async def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str) -> list: """ Search for tags via contents. @@ -112,18 +112,28 @@ class Tags(Cog): if check(query in tag['embed']['description'].casefold() for query in keywords_processed): matching_tags.append(tag) + return matching_tags + + async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None: + """Send the result of matching tags to user.""" if not matching_tags: - return None + pass elif len(matching_tags) == 1: - return Embed().from_dict(matching_tags[0]['embed']) + await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed'])) else: - is_plural = len(keywords_processed) > 1 or keywords.strip().count(' ') > 0 + is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0 embed = Embed( title=f"Here are the tags containing the given keyword{'s' * is_plural}:", description='\n'.join(tag['title'] for tag in matching_tags[:10]) ) - embed.set_footer(text=f"Keyword{'s' * is_plural} used: {keywords}"[:1024]) - return embed + await LinePaginator.paginate( + sorted(f"**ยป** {tag['title']}" for tag in matching_tags), + ctx, + embed, + footer_text="To show a tag, type !tags .", + empty=False, + max_lines=15 + ) @group(name='tags', aliases=('tag', 't'), invoke_without_command=True) async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: @@ -137,9 +147,8 @@ class Tags(Cog): Only search for tags that has ALL the keywords. """ - result = await self._get_tags_via_content(all, keywords) - if result: - await ctx.send(embed=result) + matching_tags = await self._get_tags_via_content(all, keywords) + await self._send_matching_tags(ctx, keywords, matching_tags) @search_tag_content.command(name='any') async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = None) -> None: @@ -148,9 +157,8 @@ class Tags(Cog): Search for tags that has ANY of the keywords. """ - result = await self._get_tags_via_content(any, keywords or 'any') - if result: - await ctx.send(embed=result) + matching_tags = await self._get_tags_via_content(any, keywords or 'any') + await self._send_matching_tags(ctx, keywords, matching_tags) @tags_group.command(name='get', aliases=('show', 'g')) async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: -- cgit v1.2.3