-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[v3 cog manager, utils] handle missing cogs correctly, add some helpful algorithms #1989
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ | |
from redbot.core import checks | ||
from redbot.core import i18n | ||
from redbot.core import commands | ||
from redbot.core.cog_manager import NoSuchCog | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unused import |
||
from .utils.chat_formatting import pagify, box, inline | ||
|
||
if TYPE_CHECKING: | ||
|
@@ -77,9 +78,17 @@ async def _load(self, cog_names: list): | |
for name in cog_names: | ||
try: | ||
spec = await bot.cog_mgr.find_cog(name) | ||
cogspecs.append((spec, name)) | ||
except RuntimeError: | ||
notfound_packages.append(name) | ||
if spec: | ||
cogspecs.append((spec, name)) | ||
else: | ||
notfound_packages.append(name) | ||
except Exception as e: | ||
log.exception("Package import failed", exc_info=e) | ||
|
||
exception_log = "Exception during import of cog\n" | ||
exception_log += "".join(traceback.format_exception(type(e), e, e.__traceback__)) | ||
bot._last_exception = exception_log | ||
failed_packages.append(name) | ||
|
||
for spec, name in cogspecs: | ||
try: | ||
|
@@ -95,6 +104,7 @@ async def _load(self, cog_names: list): | |
else: | ||
await bot.add_loaded_package(name) | ||
loaded_packages.append(name) | ||
|
||
return loaded_packages, failed_packages, notfound_packages | ||
|
||
def _cleanup_and_refresh_modules(self, module_name: str): | ||
|
@@ -511,7 +521,7 @@ async def load(self, ctx, *, cog_name: str): | |
loaded, failed, not_found = await self._load(cog_names) | ||
|
||
if loaded: | ||
fmt = "Loaded {packs}" | ||
fmt = "Loaded {packs}." | ||
formed = self._get_package_strings(loaded, fmt) | ||
await ctx.send(formed) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,29 @@ | ||
__all__ = ["safe_delete", "fuzzy_command_search"] | ||
__all__ = ["bounded_gather", "safe_delete", "fuzzy_command_search", "deduplicate_iterables"] | ||
|
||
from pathlib import Path | ||
import asyncio | ||
from asyncio import AbstractEventLoop, Semaphore | ||
from concurrent.futures import FIRST_COMPLETED | ||
from itertools import chain | ||
import logging | ||
import os | ||
from pathlib import Path | ||
import shutil | ||
import logging | ||
from typing import Any, AsyncIterator, List, Optional | ||
|
||
from redbot.core import commands | ||
from fuzzywuzzy import process | ||
|
||
from .chat_formatting import box | ||
|
||
# Benchmarked to be the fastest method. | ||
def deduplicate_iterables(*iterables): | ||
""" | ||
Returns a list of all unique items in ``iterables``, in the order they | ||
were first encountered. | ||
""" | ||
# dict insertion order is guaranteed to be preserved in 3.6+ | ||
return list(dict.fromkeys(chain.from_iterable(iterables))) | ||
|
||
|
||
def fuzzy_filter(record): | ||
return record.funcName != "extractWithoutOrder" | ||
|
@@ -20,10 +36,13 @@ def safe_delete(pth: Path): | |
if pth.exists(): | ||
for root, dirs, files in os.walk(str(pth)): | ||
os.chmod(root, 0o755) | ||
|
||
for d in dirs: | ||
os.chmod(os.path.join(root, d), 0o755) | ||
|
||
for f in files: | ||
os.chmod(os.path.join(root, f), 0o755) | ||
|
||
shutil.rmtree(str(pth), ignore_errors=True) | ||
|
||
|
||
|
@@ -33,35 +52,41 @@ async def filter_commands(ctx: commands.Context, extracted: list): | |
for i in extracted | ||
if i[1] >= 90 | ||
and not i[0].hidden | ||
and not any([p.hidden for p in i[0].parents]) | ||
and await i[0].can_run(ctx) | ||
and all([await p.can_run(ctx) for p in i[0].parents]) | ||
and not any([p.hidden for p in i[0].parents]) | ||
] | ||
|
||
|
||
async def fuzzy_command_search(ctx: commands.Context, term: str): | ||
out = "" | ||
out = [] | ||
|
||
if ctx.guild is not None: | ||
enabled = await ctx.bot.db.guild(ctx.guild).fuzzy() | ||
else: | ||
enabled = await ctx.bot.db.fuzzy() | ||
|
||
if not enabled: | ||
return None | ||
|
||
alias_cog = ctx.bot.get_cog("Alias") | ||
if alias_cog is not None: | ||
is_alias, alias = await alias_cog.is_alias(ctx.guild, term) | ||
|
||
if is_alias: | ||
return None | ||
|
||
customcom_cog = ctx.bot.get_cog("CustomCommands") | ||
if customcom_cog is not None: | ||
cmd_obj = customcom_cog.commandobj | ||
|
||
try: | ||
ccinfo = await cmd_obj.get(ctx.message, term) | ||
except: | ||
pass | ||
else: | ||
return None | ||
|
||
extracted_cmds = await filter_commands( | ||
ctx, process.extract(term, ctx.bot.walk_commands(), limit=5) | ||
) | ||
|
@@ -70,10 +95,105 @@ async def fuzzy_command_search(ctx: commands.Context, term: str): | |
return None | ||
|
||
for pos, extracted in enumerate(extracted_cmds, 1): | ||
out += "{0}. {1.prefix}{2.qualified_name}{3}\n".format( | ||
pos, | ||
ctx, | ||
extracted[0], | ||
" - {}".format(extracted[0].short_doc) if extracted[0].short_doc else "", | ||
) | ||
return box(out, lang="Perhaps you wanted one of these?") | ||
short = " - {}".format(extracted[0].short_doc) if extracted[0].short_doc else "" | ||
out.append("{0}. {1.prefix}{2.qualified_name}{3}".format(pos, ctx, extracted[0], short)) | ||
|
||
return box("\n".join(out), lang="Perhaps you wanted one of these?") | ||
|
||
|
||
async def bounded_gather_iter( | ||
*coros_or_futures, | ||
loop: Optional[AbstractEventLoop] = None, | ||
limit: int = 4, | ||
semaphore: Optional[Semaphore] = None, | ||
) -> AsyncIterator[Any]: | ||
""" | ||
An async iterator that returns tasks as they are ready, but limits the number of tasks running | ||
at a time. | ||
|
||
Parameters | ||
---------- | ||
*coros_or_futures | ||
The awaitables to run in a bounded concurrent fashion. | ||
loop : asyncio.AbstractEventLoop | ||
The event loop to use for the semaphore and :meth:`asyncio.gather`. | ||
limit : Optional[`int`] | ||
The maximum number of concurrent tasks. Used when no ``semaphore`` is passed. | ||
semaphore : Optional[:class:`asyncio.Semaphore`] | ||
The semaphore to use for bounding tasks. If `None`, create one using ``loop`` and ``limit``. | ||
|
||
Raises | ||
------ | ||
TypeError | ||
When invalid parameters are passed | ||
""" | ||
if loop is None: | ||
loop = asyncio.get_event_loop() | ||
|
||
if semaphore is None: | ||
if type(limit) != int or limit <= 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use |
||
raise TypeError("limit must be an int > 0") | ||
|
||
semaphore = Semaphore(limit, loop=loop) | ||
|
||
async def sem_wrapper(sem, task): | ||
async with sem: | ||
return await task | ||
|
||
pending = [sem_wrapper(semaphore, task) for task in coros_or_futures] | ||
|
||
while pending: | ||
async with semaphore: | ||
try: | ||
done, pending = await asyncio.wait(pending, loop=loop, return_when=FIRST_COMPLETED) | ||
except asyncio.CancelledError: | ||
[] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what's going on with this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I had logic here for cancelling pending tasks, but I found something better (see the push) |
||
|
||
while done: | ||
yield done.pop() | ||
|
||
|
||
def bounded_gather( | ||
*coros_or_futures, | ||
loop: Optional[AbstractEventLoop] = None, | ||
return_exceptions: bool = False, | ||
limit: int = 4, | ||
semaphore: Optional[Semaphore] = None, | ||
) -> List[Any]: | ||
""" | ||
A semaphore-bounded wrapper to :meth:`asyncio.gather`. | ||
|
||
Parameters | ||
---------- | ||
*coros_or_futures | ||
The awaitables to run in a bounded concurrent fashion. | ||
loop : asyncio.AbstractEventLoop | ||
The event loop to use for the semaphore and :meth:`asyncio.gather`. | ||
return_exceptions : bool | ||
If true, gather exceptions in the result list instead of raising. | ||
limit : Optional[`int`] | ||
The maximum number of concurrent tasks. Used when no ``semaphore`` is passed. | ||
semaphore : Optional[:class:`asyncio.Semaphore`] | ||
The semaphore to use for bounding tasks. If `None`, create one using ``loop`` and ``limit``. | ||
|
||
Raises | ||
------ | ||
TypeError | ||
When invalid parameters are passed | ||
""" | ||
if loop is None: | ||
loop = asyncio.get_event_loop() | ||
|
||
if semaphore is None: | ||
if type(limit) != int or limit <= 0: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
raise TypeError("limit must be an int > 0") | ||
|
||
semaphore = Semaphore(limit, loop=loop) | ||
|
||
async def sem_wrapper(sem, task): | ||
async with sem: | ||
return await task | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this |
||
|
||
tasks = (sem_wrapper(semaphore, task) for task in coros_or_futures) | ||
|
||
return asyncio.gather(*tasks, loop=loop, return_exceptions=return_exceptions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
paths()
already deduplicates the paths before it returns, so you can remove the call to deduplicate here