diff --git a/.gitignore b/.gitignore index 7d5b31b..a9038ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# Testing -examples/commands/ - # JetBrains .idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bea4bd9..2986fe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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.1.0] -- 2021-15-02 + +### Added +- The subclassed bot can now be run via a poetry script `poetry run example`. +- The `examples` directory now includes a sample command directory & cog file to + demonstrate the expected tree and code structure. (#9) + + ## [2.0.0] -- 2021-30-01 ### Changed diff --git a/cogwatch/__init__.py b/cogwatch/__init__.py index 5e171d6..67478a3 100644 --- a/cogwatch/__init__.py +++ b/cogwatch/__init__.py @@ -1 +1,17 @@ +""" +dpymenus -- Simplified menus for discord.py developers. +""" + +__title__ = "cogwatch" +__author__ = "Rob Wagner " +__license__ = "MIT" +__copyright__ = "Copyright 2020-2021 Rob Wagner" +__version__ = "2.1.0" + +import logging + from cogwatch.cogwatch import Watcher, watch + + +logger = logging.getLogger("cogwatch") +logger.addHandler(logging.NullHandler()) diff --git a/cogwatch/cogwatch.py b/cogwatch/cogwatch.py index 3a0056c..6cb10b1 100644 --- a/cogwatch/cogwatch.py +++ b/cogwatch/cogwatch.py @@ -8,9 +8,6 @@ from discord.ext import commands from watchgod import Change, awatch -logger = logging.getLogger("cogwatch") -logger.addHandler(logging.NullHandler()) - class Watcher: """The core cogwatch class -- responsible for starting up watchers and managing cogs. @@ -133,7 +130,7 @@ async def start(self): if self.loop is None: self.loop = asyncio.get_event_loop() - logger.info(f"Watching for file changes in {Path.cwd() / self.path}...") + logging.info(f"Watching for file changes in {Path.cwd() / self.path}...") self.loop.create_task(self._start()) async def load(self, cog_dir: str): @@ -145,7 +142,7 @@ async def load(self, cog_dir: str): except Exception as exc: self.cog_error(exc) else: - logger.info(f"Cog Loaded: {cog_dir}") + logging.info(f"Cog Loaded: {cog_dir}") async def unload(self, cog_dir: str): """Unloads a cog file into the client.""" @@ -154,7 +151,7 @@ async def unload(self, cog_dir: str): except Exception as exc: self.cog_error(exc) else: - logger.info(f"Cog Unloaded: {cog_dir}") + logging.info(f"Cog Unloaded: {cog_dir}") async def reload(self, cog_dir: str): """Attempts to atomically reload the file into the client.""" @@ -163,16 +160,16 @@ async def reload(self, cog_dir: str): except Exception as exc: self.cog_error(exc) else: - logger.info(f"Cog Reloaded: {cog_dir}") + logging.info(f"Cog Reloaded: {cog_dir}") @staticmethod def cog_error(exc: Exception): """Logs exceptions. TODO: Need thorough exception handling.""" if isinstance(exc, (commands.ExtensionError, SyntaxError)): - logger.exception(exc) + logging.exception(exc) async def _preload(self): - logger.info("Preloading...") + logging.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]])) diff --git a/examples/commands/ping.py b/examples/commands/ping.py new file mode 100644 index 0000000..8ff406c --- /dev/null +++ b/examples/commands/ping.py @@ -0,0 +1,14 @@ +from discord.ext import commands + + +class Ping(commands.Cog): + def __init__(self, client): + self.client = client + + @commands.command() + async def ping(self, ctx): + await ctx.reply("Pong!") + + +def setup(client): + client.add_cog(Ping(client)) diff --git a/poetry.lock b/poetry.lock index ae13982..849b373 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,6 +235,17 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "python-dotenv" +version = "0.15.0" +description = "Add .env support to your django/flask apps in development and deployments" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "regex" version = "2020.11.13" @@ -267,6 +278,19 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "uvloop" +version = "0.15.1" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +dev = ["Cython (>=0.29.20,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)", "aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=1.7.3,<1.8.0)", "sphinxcontrib-asyncio (>=0.2.0,<0.3.0)", "sphinx-rtd-theme (>=0.2.4,<0.3.0)"] +test = ["aiohttp", "flake8 (>=3.8.4,<3.9.0)", "psutil", "pycodestyle (>=2.6.0,<2.7.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] + [[package]] name = "watchgod" version = "0.7" @@ -303,7 +327,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "6f583049c2b292f3322b06140d9e11e7aaf850ffa6903c4cc8c9ab6b217dd929" +content-hash = "7ba217edc3c47fbf1c6110db6ed1766962a27b121d2aa57099f2c7dadc3b2625" [metadata.files] aiohttp = [ @@ -459,6 +483,10 @@ pytest = [ {file = "pytest-6.2.2-py3-none-any.whl", hash = "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"}, {file = "pytest-6.2.2.tar.gz", hash = "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9"}, ] +python-dotenv = [ + {file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"}, + {file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"}, +] regex = [ {file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"}, {file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"}, @@ -543,6 +571,18 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] +uvloop = [ + {file = "uvloop-0.15.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9541dc3f391941796ae95c9c3bb16b813acf9e3d4beebfd3b623f1acb22d318d"}, + {file = "uvloop-0.15.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e178c255622d928d464187e3ceba94db88465f6b17909c651483fb73af8d8b85"}, + {file = "uvloop-0.15.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:47ec567151070ed770211d359ad9250b59368548c60212c7ef6dda3f5b1778f6"}, + {file = "uvloop-0.15.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:66881fe8a2187334c4dd5010c56310bdf32fe426613f9ca727f090bc31280624"}, + {file = "uvloop-0.15.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:e72779681f839b6a069d7e7a9f7962a1d1927612c5c2e33071415478bdc1b91b"}, + {file = "uvloop-0.15.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:ed073d24e0c383c24d17d3a2bb209b999ff0a8130e89b7c3f033db9e0c3bd04f"}, + {file = "uvloop-0.15.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:236a3c31096e0845029856f7bc07a938340c2cdb35d9d39b38c9253b672bf948"}, + {file = "uvloop-0.15.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ca8a9e982f0bfbe331f41902cdd721c6e749e4685a403685e792b86a584f5969"}, + {file = "uvloop-0.15.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1ae1ad731c8c0dcee80e0ecf06274f0f7293244d2cef81fa2747321a370a6aba"}, + {file = "uvloop-0.15.1.tar.gz", hash = "sha256:7846828112bfb49abc5fdfc47d0e4dfd7402115c9fde3c14c31818cfbeeb63dc"}, +] watchgod = [ {file = "watchgod-0.7-py3-none-any.whl", hash = "sha256:d6c1ea21df37847ac0537ca0d6c2f4cdf513562e95f77bb93abbcf05573407b7"}, {file = "watchgod-0.7.tar.gz", hash = "sha256:48140d62b0ebe9dd9cf8381337f06351e1f2e70b2203fa9c6eff4e572ca84f29"}, diff --git a/pyproject.toml b/pyproject.toml index e6391eb..f8f1c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cogwatch" -version = "2.0.0" +version = "2.1.0" description = "Automatic hot-reloading for your discord.py command files." authors = ["Rob Wagner <13954303+robertwayne@users.noreply.github.com>"] license = "MIT" @@ -16,14 +16,19 @@ classifiers=[ 'Development Status :: 5 - Production/Stable', ] +[tool.poetry.scripts] +example = 'runner:__poetry_run' + [tool.poetry.dependencies] python = "^3.7" "discord.py" = "^1.5" watchgod = "0.7" [tool.poetry.dev-dependencies] -pytest = "^6.1.2" +pytest = "^6.1" black = "^20.8b1" +python-dotenv = '^0.15' +uvloop = "^0.15.1" [tool.black] line-length = 120 diff --git a/runner.py b/runner.py new file mode 100644 index 0000000..8ce3f99 --- /dev/null +++ b/runner.py @@ -0,0 +1,57 @@ +# This is a testing module which interfaces with the documentation examples. You will need to +# have an environment variable called "COGWATCH_BOT_TOKEN" set in order for this to run. This +# module must live in the root directory. +# +# You can run this file directly or with `poetry run example`. + +import asyncio +import logging +import os +import sys + +from cogwatch import watch +from discord.ext import commands +from dotenv import load_dotenv + +try: + import uvloop +except ImportError: + uvloop = None + +logging.basicConfig(level=logging.INFO) + +if sys.platform in {"linux", "macos"}: + uvloop.install() + logging.info("Using `uvloop` asyncio event loop.") + +load_dotenv() + + +class ExampleRunner(commands.Bot): + def __init__(self): + super().__init__(command_prefix=".") + + @watch(path="examples/commands", preload=True) + async def on_ready(self): + logging.info("Bot ready.") + + async def on_message(self, message): + logging.info(message) + + if message.author.bot: + return + + await self.process_commands(message) + + +async def main(): + client = ExampleRunner() + await client.start(os.getenv("COGWATCH_BOT_TOKEN")) + + +def __poetry_run(): + asyncio.run(main()) + + +if __name__ == "__main__": + asyncio.run(main())