Skip to content

Commit

Permalink
Merge pull request #8 from robertwayne/next
Browse files Browse the repository at this point in the history
v2.0.0
  • Loading branch information
Rob authored Jan 30, 2021
2 parents 182da48 + 0b6000f commit f9ced4c
Show file tree
Hide file tree
Showing 9 changed files with 412 additions and 147 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Testing
examples/commands/

# JetBrains
.idea/

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.0.0] -- 2021-30-01

### Changed
- **BREAKING**: The `cogs_path` parameter is now just `path`. This was done for simplicity and to
closer match the general naming convention.
- Updated `watchgod` dependency to latest version for performance improvements.

## [1.1.8] -- 2020-12-14

### Changed
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class ExampleBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix='!')

@watch(cogs_path='commands')
@watch(path='commands')
async def on_ready(self):
print('Bot ready.')

Expand All @@ -56,7 +56,7 @@ if __name__ == '__main__':
**NOTE:** `cogwatch` will only run if the **\_\_debug\_\_** flag is set on Python. You can read more about that
[here](https://docs.python.org/3/library/constants.html). In short, unless you run Python with the *-O* flag from
your command line, **\_\_debug\_\_** will be **True**. If you just want to bypass this feature, pass in `debug=False` and
it won't matter if the flag is enabled or not. *This is a development tool. You should not run it on production.*
it won't matter if the flag is enabled or not.

#### Using a Classless Bot
If you are using a classless bot you cannot use the decorator method and instead must manually create your watcher.
Expand All @@ -72,27 +72,28 @@ client = commands.Bot(command_prefix='!')
async def on_ready():
print('Bot ready.')

watcher = Watcher(client, cogs_path='commands')
watcher = Watcher(client, path='commands')
await watcher.start()


client.run('YOUR_TOKEN_GOES_HERE')
```

### Configuration
You can pass any of these values to the decorator:

**cogs_path='commands'**: Root name of the cogs directory; cogwatch will only watch within this directory -- recursively.
**path='commands'**: Root name of the cogs directory; cogwatch will only watch within this directory -- recursively.

**debug=True**: Whether to run the bot only when the Python **\_\_debug\_\_** flag is True. Defaults to True.

**loop=None**: Custom event loop. Defaults to the current running event loop.

**default_logger=True**: Whether to use the default logger *(to sys.stdout)* or not. Defaults to True.

**preload=False**: Whether to detect and load all found cogs on startup. Defaults to False.
**preload=False**: Whether to detect and load all found cogs on start. Defaults to False.

### Logging
By default the utility has a logger configured so users can get output to the console. You can disable this by
By default, the utility has a logger configured so users can get output to the console. You can disable this by
passing in `default_logger=False`. If you want to hook into the logger -- for example, to pipe your output to another
terminal or `tail` a file -- you can set up a custom logger like so:

Expand Down
58 changes: 34 additions & 24 deletions cogwatch/cogwatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from discord.ext import commands
from watchgod import Change, awatch

logger = logging.getLogger('cogwatch')
logger = logging.getLogger("cogwatch")
logger.addHandler(logging.NullHandler())


Expand All @@ -17,17 +17,24 @@ class Watcher:
Attributes
:client: A discord Bot client.
:cogs_path: Root name of the cogs directory; cogwatch will only watch within this directory -- recursively.
:path: Root name of the cogs directory; cogwatch will only watch within this directory -- recursively.
:debug: Whether to run the bot only when the debug flag is True. Defaults to True.
:loop: Custom event loop. If not specified, will use the current running event loop.
:default_logger: Whether to use the default logger (to sys.stdout) or not. Defaults to True.
:preload: Whether to detect and load all found cogs on startup. Defaults to False.
"""

def __init__(self, client: commands.Bot, cogs_path: str = 'commands', debug: bool = True,
loop: asyncio.BaseEventLoop = None, default_logger: bool = True, preload: bool = False):
def __init__(
self,
client: commands.Bot,
path: str = "commands",
debug: bool = True,
loop: asyncio.BaseEventLoop = None,
default_logger: bool = True,
preload: bool = False,
):
self.client = client
self.cogs_path = cogs_path
self.path = path
self.debug = debug
self.loop = loop
self.default_logger = default_logger
Expand All @@ -37,7 +44,7 @@ def __init__(self, client: commands.Bot, cogs_path: str = 'commands', debug: boo
_default = logging.getLogger(__name__)
_default.setLevel(logging.INFO)
_default_handler = logging.StreamHandler(sys.stdout)
_default_handler.setFormatter(logging.Formatter('[%(name)s] %(message)s'))
_default_handler.setFormatter(logging.Formatter("[%(name)s] %(message)s"))
_default.addHandler(_default_handler)

@staticmethod
Expand All @@ -52,21 +59,21 @@ def get_dotted_cog_path(self, path: str) -> str:
tokens = _path.split(os.sep)
rtokens = list(reversed(tokens))

# iterate over the list backwards in order to get the first occurence in cases where a duplicate
# iterate over the list backwards in order to get the first occurrence in cases where a duplicate
# name exists in the path (ie. example_proj/example_proj/commands)
try:
root_index = rtokens.index(self.cogs_path.split('/')[0]) + 1
root_index = rtokens.index(self.path.split("/")[0]) + 1
except ValueError:
raise ValueError('Use forward-slash delimiter in your `cogs_path` parameter.')
raise ValueError("Use forward-slash delimiter in your `path` parameter.")

return '.'.join([token for token in tokens[-root_index:-1]])
return ".".join([token for token in tokens[-root_index:-1]])

async def _start(self):
"""Starts a watcher, monitoring for any file changes and dispatching event-related methods appropriatly."""
"""Starts a watcher, monitoring for any file changes and dispatching event-related methods appropriately."""
while self.dir_exists():
try:
async for changes in awatch(Path.cwd() / self.cogs_path):
self.validate_dir() # cannot figure out how to validate within awatch; some anamolies but it does work...
async for changes in awatch(Path.cwd() / self.path):
self.validate_dir() # cannot figure out how to validate within awatch; some anomalies but it does work...

reverse_ordered_changes = sorted(changes, reverse=True)

Expand All @@ -77,7 +84,7 @@ async def _start(self):
filename = self.get_cog_name(change_path)

new_dir = self.get_dotted_cog_path(change_path)
cog_dir = f'{new_dir}.{filename.lower()}' if new_dir else f'{self.cogs_path}.{filename.lower()}'
cog_dir = f"{new_dir}.{filename.lower()}" if new_dir else f"{self.path}.{filename.lower()}"

if change_type == Change.deleted:
await self.unload(cog_dir)
Expand All @@ -101,7 +108,7 @@ def check_debug(self):

def dir_exists(self):
"""Predicate method for checking whether the specified dir exists."""
return Path(Path.cwd() / self.cogs_path).exists()
return Path(Path.cwd() / self.path).exists()

def validate_dir(self):
"""Method for raising a FileNotFound error when the specified directory does not exist."""
Expand All @@ -114,19 +121,19 @@ async def start(self):
_check = False
while not self.dir_exists():
if not _check:
logging.error(f'The path {Path.cwd() / self.cogs_path} does not exist.')
logging.error(f"The path {Path.cwd() / self.path} does not exist.")
_check = True

else:
logging.info(f'Found {Path.cwd() / self.cogs_path}!')
logging.info(f"Found {Path.cwd() / self.path}!")
if self.preload:
await self._preload()

if self.check_debug():
if self.loop is None:
self.loop = asyncio.get_event_loop()

logger.info(f'Watching for file changes in {Path.cwd() / self.cogs_path}...')
logger.info(f"Watching for file changes in {Path.cwd() / self.path}...")
self.loop.create_task(self._start())

async def load(self, cog_dir: str):
Expand All @@ -138,7 +145,7 @@ async def load(self, cog_dir: str):
except Exception as exc:
self.cog_error(exc)
else:
logger.info(f'Cog Loaded: {cog_dir}')
logger.info(f"Cog Loaded: {cog_dir}")

async def unload(self, cog_dir: str):
"""Unloads a cog file into the client."""
Expand All @@ -147,7 +154,7 @@ async def unload(self, cog_dir: str):
except Exception as exc:
self.cog_error(exc)
else:
logger.info(f'Cog Unloaded: {cog_dir}')
logger.info(f"Cog Unloaded: {cog_dir}")

async def reload(self, cog_dir: str):
"""Attempts to atomically reload the file into the client."""
Expand All @@ -156,7 +163,7 @@ async def reload(self, cog_dir: str):
except Exception as exc:
self.cog_error(exc)
else:
logger.info(f'Cog Reloaded: {cog_dir}')
logger.info(f"Cog Reloaded: {cog_dir}")

@staticmethod
def cog_error(exc: Exception):
Expand All @@ -165,20 +172,23 @@ def cog_error(exc: Exception):
logger.exception(exc)

async def _preload(self):
logger.info('Preloading...')
for cog in {(file.stem, file) for file in Path(Path.cwd() / self.cogs_path).rglob('*.py')}:
logger.info("Preloading...")
for cog in {(file.stem, file) for file in Path(Path.cwd() / self.path).rglob("*.py")}:
new_dir = self.get_dotted_cog_path(cog[1])
await self.load('.'.join([new_dir, cog[0]]))
await self.load(".".join([new_dir, cog[0]]))


def watch(**kwargs):
"""Instantiates a watcher by hooking into a Bot client methods' `self` attribute."""

def decorator(function):
@wraps(function)
async def wrapper(client):
cw = Watcher(client, **kwargs)
await cw.start()
retval = await function(client)
return retval

return wrapper

return decorator
9 changes: 5 additions & 4 deletions examples/classless_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from cogwatch import Watcher

client = commands.Bot(command_prefix='!')
client = commands.Bot(command_prefix="!")


@client.event
async def on_ready():
print('Bot ready.')
print("Bot ready.")

watcher = Watcher(client, cogs_path='commands', preload=True)
watcher = Watcher(client, path="commands", preload=True)
await watcher.start()

client.run('YOUR_TOKEN_GOES_HERE')

client.run("YOUR_TOKEN_GOES_HERE")
11 changes: 6 additions & 5 deletions examples/subclassed_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

class ExampleBot(commands.Bot):
def __init__(self):
super().__init__(command_prefix='!')
super().__init__(command_prefix="!")

@watch(cogs_path='commands', debug=False)
@watch(path="commands", debug=False)
async def on_ready(self):
print('Bot ready.')
print("Bot ready.")

async def on_message(self, message):
if message.author.bot:
Expand All @@ -22,7 +22,8 @@ async def on_message(self, message):

async def main():
client = ExampleBot()
await client.start('YOUR_TOKEN_GOES_HERE')
await client.start("YOUR_TOKEN_GOES_HERE")

if __name__ == '__main__':

if __name__ == "__main__":
asyncio.run(main())
Loading

0 comments on commit f9ced4c

Please sign in to comment.