This repository has been archived by the owner on Mar 13, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 24
feat: Add jurigged hot reloading to debug extension #652
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
b7600d3
feat: Add jurigged hot reloading to debug extension
zevaryx 6226c12
fix: Add jurigged to requirements.txt
zevaryx 2efcd72
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] c87f88e
refactor: Move jurigged to be separate from debug
zevaryx 7688e8c
docs: Add documentation about jurigged
zevaryx 857d0a7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 5db19c1
refactor: Move get_all_commands to better location
zevaryx 8684eec
Merge branch 'jurigged' of github.com:discord-snake-pit/dis-snek into…
zevaryx 3906f72
fix: Move get_all_commands to naff.ext.jurigged
zevaryx 3d11008
Merge 2.x into jurigged
zevaryx 9f6ec20
fix: Add jurigged extra, and better error when loading extension wit…
silasary 1c3d93e
Rename Jurigged guide to Live Patching
zevaryx 00cc7b6
Merge branch 'jurigged' of github.com:discord-snake-pit/dis-snek into…
zevaryx 4afd84d
fix: jurigged as optional requirement
zevaryx 6fba705
Merge branch '2.x' into jurigged
silasary File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
::: naff.ext.jurigged |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Live Patching | ||
|
||
NAFF has a few built-in extensions that add some features, primarily for debugging. One of these extensions that you can enable separately is to add [`jurigged`](https://github.com/breuleux/jurigged) for live patching of code. | ||
|
||
## How to enable | ||
|
||
```py | ||
bot.load_extension("naff.ext.jurigged") | ||
``` | ||
|
||
That's it! The extension will handle all of the leg work, and all you'll notice is that you have more messages in your logs (depending on the log level). | ||
|
||
## What is jurigged? | ||
|
||
`jurigged` is a library written to allow code hot reloading in Python. It allows you to edit code and have it automagically be updated in your program the next time it is run. The code under the hood is extremely complicated, but the interface to use it is relatively simple. | ||
|
||
## How is this useful? | ||
|
||
NAFF takes advantage of jurigged to reload any and all commands that were edited whenever a change is made, allowing you to have more uptime with while still adding/improving features of your bot. |
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
import inspect | ||
from pathlib import Path | ||
from types import ModuleType | ||
from typing import Callable, Dict | ||
|
||
from naff import Extension, SlashCommand, listen | ||
from naff.client.errors import ExtensionLoadException, ExtensionNotFound | ||
from naff.client.utils.misc_utils import find | ||
from naff.client.const import get_logger | ||
|
||
try: | ||
from jurigged import watch, CodeFile | ||
from jurigged.live import WatchOperation | ||
from jurigged.codetools import ( | ||
AddOperation, | ||
DeleteOperation, | ||
UpdateOperation, | ||
LineDefinition, | ||
) | ||
except ModuleNotFoundError: | ||
get_logger().error( | ||
"jurigged not installed, cannot enable jurigged integration. Install with `pip install naff[jurigged]`" | ||
) | ||
raise | ||
|
||
|
||
__all__ = ("Jurigged", "setup") | ||
|
||
|
||
def get_all_commands(module: ModuleType) -> Dict[str, Callable]: | ||
""" | ||
Get all SlashCommands from a specified module. | ||
|
||
Args: | ||
module: Module to extract commands from | ||
""" | ||
commands = {} | ||
|
||
def is_extension(e) -> bool: | ||
"""Check that an object is an extension.""" | ||
return inspect.isclass(e) and issubclass(e, Extension) and e is not Extension | ||
|
||
def is_slashcommand(e) -> bool: | ||
"""Check that an object is a slash command.""" | ||
return isinstance(e, SlashCommand) | ||
|
||
for _name, item in inspect.getmembers(module, is_extension): | ||
inspect_result = inspect.getmembers(item, is_slashcommand) | ||
exts = [] | ||
for _, val in inspect_result: | ||
exts.append(val) | ||
commands[f"{module.__name__}"] = exts | ||
|
||
return {k: v for k, v in commands.items() if v is not None} | ||
|
||
|
||
class Jurigged(Extension): | ||
@listen(event_name="on_startup") | ||
async def jurigged_startup(self) -> None: | ||
"""Jurigged starting utility.""" | ||
self.command_cache = {} | ||
self.bot.logger.warning("Setting sync_ext to True by default for syncing changes") | ||
self.bot.sync_ext = True | ||
|
||
self.bot.logger.info("Loading jurigged") | ||
path = Path().resolve() | ||
self.watcher = watch(f"{path}/[!.]*.py", logger=self.jurigged_log) | ||
self.watcher.prerun.register(self.jurigged_prerun) | ||
self.watcher.postrun.register(self.jurigged_postrun) | ||
|
||
def jurigged_log(self, event: WatchOperation | AddOperation | DeleteOperation | UpdateOperation) -> None: | ||
""" | ||
Log a jurigged event | ||
|
||
Args: | ||
event: jurigged event | ||
""" | ||
if isinstance(event, WatchOperation): | ||
self.bot.logger.debug(f"Watch {event.filename}") | ||
elif isinstance(event, (Exception, SyntaxError)): | ||
self.bot.logger.exception("Jurigged encountered an error", exc_info=True) | ||
else: | ||
event_str = "{action} {dotpath}:{lineno}{extra}" | ||
action = None | ||
lineno = event.defn.stashed.lineno | ||
dotpath = event.defn.dotpath() | ||
extra = "" | ||
|
||
if isinstance(event.defn, LineDefinition): | ||
dotpath = event.defn.parent.dotpath() | ||
extra = f" | {event.defn.text}" | ||
|
||
if isinstance(event, AddOperation): | ||
action = "Add" | ||
if isinstance(event.defn, LineDefinition): | ||
action = "Run" | ||
elif isinstance(event, UpdateOperation): | ||
action = "Update" | ||
elif isinstance(event, DeleteOperation): | ||
action = "Delete" | ||
if not action: | ||
self.bot.logger.debug(event) | ||
else: | ||
self.bot.logger.debug(event_str.format(action=action, dotpath=dotpath, lineno=lineno, extra=extra)) | ||
|
||
def jurigged_prerun(self, _path: str, cf: CodeFile) -> None: | ||
""" | ||
Jurigged prerun event. | ||
|
||
Args: | ||
path: Path to file | ||
cf: File information | ||
""" | ||
if self.bot.get_ext(cf.module_name): | ||
self.bot.logger.debug(f"Caching {cf.module_name}") | ||
self.command_cache = get_all_commands(cf.module) | ||
|
||
def jurigged_postrun(self, _path: str, cf: CodeFile) -> None: | ||
""" | ||
Jurigged postrun event. | ||
|
||
Args: | ||
path: Path to file | ||
cf: File information | ||
""" | ||
if self.bot.get_ext(cf.module_name): | ||
self.bot.logger.debug(f"Checking {cf.module_name}") | ||
commands = get_all_commands(cf.module) | ||
|
||
self.bot.logger.debug("Checking for changes") | ||
for module, cmds in commands.items(): | ||
# Check if a module was removed | ||
if module not in commands: | ||
self.bot.logger.debug(f"Module {module} removed") | ||
self.bot.unload_extension(module) | ||
|
||
# Check if a module is new | ||
elif module not in self.command_cache: | ||
self.bot.logger.debug(f"Module {module} added") | ||
try: | ||
self.bot.load_extension(module) | ||
except ExtensionLoadException: | ||
self.bot.logger.warning(f"Failed to load new module {module}") | ||
|
||
# Check if a module has more/less commands | ||
elif len(self.command_cache[module]) != len(cmds): | ||
self.bot.logger.debug("Number of commands changed, reloading") | ||
try: | ||
self.bot.reload_extension(module) | ||
except ExtensionNotFound: | ||
try: | ||
self.bot.load_extension(module) | ||
except ExtensionLoadException: | ||
self.bot.logger.warning(f"Failed to update module {module}") | ||
except ExtensionLoadException: | ||
self.bot.logger.warning(f"Failed to update module {module}") | ||
|
||
# Check each command for differences | ||
else: | ||
for cmd in cmds: | ||
old_cmd = find( | ||
lambda x, cmd=cmd: x.resolved_name == cmd.resolved_name, | ||
self.command_cache[module], | ||
) | ||
|
||
# Extract useful info | ||
old_args = old_cmd.options | ||
old_arg_names = [] | ||
new_arg_names = [] | ||
if old_args: | ||
old_arg_names = [x.name.default for x in old_args] | ||
new_args = cmd.options | ||
if new_args: | ||
new_arg_names = [x.name.default for x in new_args] | ||
|
||
# No changes | ||
if not old_args and not new_args: | ||
continue | ||
|
||
# Check if number of args has changed | ||
if len(old_arg_names) != len(new_arg_names): | ||
self.bot.logger.debug("Number of arguments changed, reloading") | ||
try: | ||
self.bot.reload_extension(module) | ||
except Exception: | ||
self.bot.logger.exception(f"Failed to update module {module}", exc_info=True) | ||
|
||
# Check if arg names have changed | ||
elif len(set(old_arg_names) - set(new_arg_names)) > 0: | ||
self.bot.logger.debug("Argument names changed, reloading") | ||
try: | ||
self.bot.reload_extension(module) | ||
except Exception: | ||
self.bot.logger.exception(f"Failed to update module {module}", exc_info=True) | ||
|
||
# Check if arg types have changed | ||
elif any(new_args[idx].type != x.type for idx, x in enumerate(old_args)): | ||
self.bot.logger.debug("Argument types changed, reloading") | ||
try: | ||
self.bot.reload_extension(module) | ||
except Exception: | ||
self.bot.logger.exception(f"Failed to update module {module}", exc_info=True) | ||
else: | ||
self.bot.logger.debug("No changes detected") | ||
self.command_cache.clear() | ||
|
||
|
||
def setup(bot) -> None: | ||
Jurigged(bot) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
This name means nothing to people who don't already know the library. I'd rather call it "Live Editing" or "Hot Reloading" or something similar.
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.
I think "Live Patching" is a better name
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.
However, for the API reference, keeping the
jurigged
name should be fine; I'll update the other parts of the docs to reference it as Live Patching