diff --git a/README.md b/README.md index 17f49203..12e47c1b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,55 @@ [![Github CI](https://github.com/yoda-pa/yoda/actions/workflows/ci.yml/badge.svg)](https://github.com/yoda-pa/yoda/actions/workflows/ci.yml) [![PyPI version](https://badge.fury.io/py/yodapa.svg)](https://badge.fury.io/py/yodapa) +Personal Assistant on the command line. + +## Installation + +```bash +pip install yodapa + +yoda --help +``` + +## Configure Yoda + +```bash +yoda configure +``` + +## Plugins + +### Write your own plugin for Yoda + +Simply create a class with the `@yoda_plugin(name="plugin-name")` decorator and add methods to it. The non-private +methods will be automatically added as sub-commands to Yoda, with the command being the name you provide to the +decorator. + +```python +import typer +from yodapa.plugin_manager.decorator import yoda_plugin + + +@yoda_plugin(name="hi") +class HiPlugin: + """ + Hi plugin. Say hello. + + Example: + $ yoda hi hello --name MP + $ yoda hi hello + """ + + def hello(self, name: str = None): + """Say hello.""" + name = name or "Padawan" + typer.echo(f"Hello {name}!") + + def _private_method_should_not_be_added(self): + """This method should not be added as a command.""" + raise NotImplementedError() +``` + ## Development setup ```bash diff --git a/poetry.lock b/poetry.lock index 1ea66103..f69fd2f9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -147,20 +147,83 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "rich" -version = "13.8.1" +version = "13.9.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" files = [ - {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, - {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, + {file = "rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1"}, + {file = "rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -178,13 +241,13 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] @@ -218,4 +281,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "6f001741a3a0276d12ecf1fddaf9dd97868be6b4a86aff5734729a2d53809d39" +content-hash = "14b41c511d53e7d5b9dc4642f6ba2adca0a8ebec55d4d64b134f809331fa91b0" diff --git a/pyproject.toml b/pyproject.toml index cfdda13e..1607f998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "yodapa" -version = "0.1.3" +version = "0.2.0" description = "Personal Assistant on the command line" authors = ["Man Parvesh Singh Randhawa "] license = "MIT" @@ -15,6 +15,7 @@ homepage = "https://yoda-pa.github.io/" python = "^3.9" typer = "^0.12.5" pytest = "^8.3.3" +pyyaml = "^6.0.2" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" diff --git a/src/cli/yoda.py b/src/cli/yoda.py index 9a4c4bb6..e23ffe37 100644 --- a/src/cli/yoda.py +++ b/src/cli/yoda.py @@ -1,22 +1,43 @@ +from typing import Annotated, Optional + import typer -from yodapa import hi -from yodapa.hi import add_numbers +from yodapa.config import ConfigManager +from yodapa.plugin_manager.plugin import PluginManager + + +class Yoda: + """Yoda main class.""" + + def __init__(self): + self.app = typer.Typer() + self.config = ConfigManager() + self.plugin_manager = PluginManager(self.app, self.config) + + def init(self): + self.plugin_manager.discover_plugins() + self.plugin_manager.load_plugins() + + +yoda = Yoda() +yoda.init() -app = typer.Typer() +# define commands +app = yoda.app @app.command() -def hello(name: str): - """Greet someone by name.""" - typer.echo(hi.hu(name)) +def hello(name: Annotated[Optional[str], typer.Argument()] = None): + """Say hello.""" + name = name or yoda.config.get("user", "Skywalker") + typer.echo(f"Hello {name}!") @app.command() -def add(a: int, b: int): - """Add two numbers.""" - result = add_numbers(a, b) - typer.echo(f"The sum of {a} and {b} is {result}") +def configure(): + """Configure Yoda.""" + yoda.config.set("user", typer.prompt("What is your name?")) + typer.echo("Yoda configured!") if __name__ == "__main__": diff --git a/src/yodapa/config.py b/src/yodapa/config.py new file mode 100644 index 00000000..2fa2eb0a --- /dev/null +++ b/src/yodapa/config.py @@ -0,0 +1,51 @@ +from pathlib import Path +from typing import Any, Dict + +import yaml + + +class ConfigManager: + """Configuration manager class. Manages the configuration of Yoda.""" + + def __init__(self): + self.config_file = self.get_default_config_file() + self.config: Dict[str, Any] = dict() + self.load() + + def load(self): + """Load the configuration from the configuration file.""" + if self.config_file.exists(): + with open(self.config_file) as f: + self.config = yaml.safe_load(f) + else: + self.config = self.get_default_config() + + def save(self): + """Save the configuration to the configuration file.""" + self.config_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.config_file, "w") as f: + yaml.safe_dump(self.config, f) + + def get(self, key: str, default: Any = None) -> Any: + """Get a configuration value by key.""" + return self.config.get(key, default) + + def set(self, key: str, value: Any): + """Set a configuration value by key.""" + self.config[key] = value + self.save() + + def get_yoda_config_dir(self) -> Path: + return Path.home() / ".yoda" + + def get_yoda_plugins_dir(self) -> Path: + return self.get_yoda_config_dir() / "plugins" + + def get_default_config_file(self) -> Path: + return self.get_yoda_config_dir() / "config.yaml" + + def get_default_config(self) -> Dict[str, Any]: + return { + "user": "Skywalker", + "plugins": {}, + } diff --git a/src/yodapa/hi.py b/src/yodapa/hi.py deleted file mode 100644 index 27fee010..00000000 --- a/src/yodapa/hi.py +++ /dev/null @@ -1,9 +0,0 @@ -from yodapa.utils import add - - -def hu(s: str) -> str: - return f"Hellowww {s}" - - -def add_numbers(a: int, b: int) -> int: - return add(a, b) diff --git a/src/yodapa/plugin_manager/__init__.py b/src/yodapa/plugin_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/yodapa/plugin_manager/decorator.py b/src/yodapa/plugin_manager/decorator.py new file mode 100644 index 00000000..3e2d6dcc --- /dev/null +++ b/src/yodapa/plugin_manager/decorator.py @@ -0,0 +1,31 @@ +import inspect +from typing import Annotated, Optional + +import typer + + +def yoda_plugin(name: Annotated[Optional[str], typer.Argument()] = None): + """ + Decorator to turn a class into a Yoda PA plugin. + All public methods of the class are added as Typer commands. + """ + + def decorator(cls): + nonlocal name + name = name or cls.__name__.lower() + + def __init__(self): + self.typer_app = typer.Typer(name=name, help=f"{name} plugin commands") + + for method_name, method in inspect.getmembers(self, predicate=inspect.ismethod): + # Skip private methods + if method_name.startswith("_"): + continue + + self.typer_app.command()(method) + + cls.__init__ = __init__ + cls.name = name + return cls + + return decorator diff --git a/src/yodapa/plugin_manager/plugin.py b/src/yodapa/plugin_manager/plugin.py new file mode 100644 index 00000000..dbb98ab6 --- /dev/null +++ b/src/yodapa/plugin_manager/plugin.py @@ -0,0 +1,87 @@ +import importlib +import inspect +import pkgutil +from typing import List + +import typer + +from yodapa.config import ConfigManager + + +class PluginManager: + """Plugin manager class. Manages the plugins in the 'plugins' directory and the local plugins directory.""" + + def __init__(self, app: typer.Typer, config: ConfigManager): + self.app: typer.Typer = app + self.config: ConfigManager = config + self.plugins: List = [] + + def discover_plugins(self): + """Discover plugins in the 'plugins' directory and the local plugins directory.""" + + # 1. Discover plugins within the 'plugins' directory + plugins_pkg = importlib.import_module('yodapa.plugins') + plugins_path = plugins_pkg.__path__ + + for finder, name, ispkg in pkgutil.iter_modules(plugins_path): + # print("finder", finder, "name", name, "ispkg", ispkg) + try: + module = importlib.import_module(f'yodapa.plugins.{name}') + for attribute_name in dir(module): + plugin_class = getattr(module, attribute_name) + if inspect.isclass(plugin_class) and hasattr(plugin_class(), "typer_app"): + plugin_instance = plugin_class() + self.plugins.append(plugin_instance) + except Exception as e: + typer.echo(f"Failed to load plugin {name}: {e}", err=True) + + # 2. Discover plugins in the local plugins directory + local_plugins_dir = self.config.get_yoda_plugins_dir() + if local_plugins_dir.exists() and local_plugins_dir.is_dir(): + for finder, name, ispkg in pkgutil.iter_modules([str(local_plugins_dir)]): + # print("finder", finder, "name", name, "ispkg", ispkg) + try: + module_path = local_plugins_dir / f"{name}.py" + if not module_path.exists(): + typer.echo(f"Plugin module {name} does not exist at {module_path}", err=True) + continue + + # Load the module from the file path + spec = importlib.util.spec_from_file_location(name, str(module_path)) + if spec is None: + typer.echo(f"Could not load spec for module {name}", err=True) + continue + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # typer.echo(f"Imported module: {module.__name__}") + + # Find the plugin class in the module (same as above) + for attribute_name in dir(module): + plugin_class = getattr(module, attribute_name) + if inspect.isclass(plugin_class) and hasattr(plugin_class(), "typer_app"): + plugin_instance = plugin_class() + self.plugins.append(plugin_instance) + except Exception as e: + typer.echo(f"Failed to load local plugin {name}: {e}", err=True) + + # uncomment to debug + # print("Plugins discovered:", self.plugins) + + def load_plugins(self): + """Load the plugins into the yoda typer app.""" + for plugin in self.plugins: + try: + self.app.add_typer(plugin.typer_app, name=plugin.name, help=f"{plugin.name} plugin commands") + # typer.echo(f"Loaded plugin: {plugin.name}") + except Exception as e: + typer.echo(f"Error loading plugin {plugin.name}: {e}", err=True) + + def enable_plugin(self, plugin_name: str): + # TODO: implement + pass + + def disable_plugin(self, plugin_name: str): + # TODO: implement + pass diff --git a/src/yodapa/plugins/__init__.py b/src/yodapa/plugins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/yodapa/plugins/bye_plugin.py b/src/yodapa/plugins/bye_plugin.py new file mode 100644 index 00000000..48c7303f --- /dev/null +++ b/src/yodapa/plugins/bye_plugin.py @@ -0,0 +1,19 @@ +import typer + +from yodapa.plugin_manager.decorator import yoda_plugin + + +@yoda_plugin(name="bye") +class ByePlugin: + """ + Bye plugin. Say goodbye. + + Example: + $ yoda bye goodbye --name MP + $ yoda bye goodbye + """ + + def goodbye(self, name: str = None): + """Say goodbye.""" + name = name or "Padawan" + typer.echo(f"Goodbye {name}!") diff --git a/src/yodapa/plugins/hi_plugin.py b/src/yodapa/plugins/hi_plugin.py new file mode 100644 index 00000000..4b127e71 --- /dev/null +++ b/src/yodapa/plugins/hi_plugin.py @@ -0,0 +1,23 @@ +import typer + +from yodapa.plugin_manager.decorator import yoda_plugin + + +@yoda_plugin(name="hi") +class HiPlugin: + """ + Hi plugin. Say hello. + + Example: + $ yoda hi hello --name MP + $ yoda hi hello + """ + + def hello(self, name: str = None): + """Say hello.""" + name = name or "Padawan" + typer.echo(f"Hello {name}!") + + def _private_method_should_not_be_added(self): + """This method should not be added as a command.""" + raise NotImplementedError() diff --git a/src/yodapa/utils.py b/src/yodapa/utils.py deleted file mode 100644 index e1829c35..00000000 --- a/src/yodapa/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def add(a: int, b: int) -> int: - return a + b diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index a8edc06d..00000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,9 +0,0 @@ -from yodapa.hi import hu, add_numbers - - -def test_greet(): - assert hu("Alice") == "Hellowww Alice" - - -def test_add_numbers(): - assert add_numbers(3, 4) == 7 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..7dbcf851 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,46 @@ +from pathlib import Path + +from yodapa.config import ConfigManager + +config = ConfigManager() + + +def test_default_config(): + default_config = config.get_default_config() + assert default_config.get("user") == "Skywalker" + assert default_config.get("plugins") == {} + + +def test_get_yoda_config_dir(): + yoda_config_dir = config.get_yoda_config_dir() + assert yoda_config_dir == Path.home() / ".yoda" + + +def test_get_yoda_plugins_dir(): + yoda_plugins_dir = config.get_yoda_plugins_dir() + assert yoda_plugins_dir == Path.home() / ".yoda" / "plugins" + + +def test_get_default_config_file(): + default_config_file = config.get_default_config_file() + assert default_config_file == Path.home() / ".yoda" / "config.yaml" + + +def test_load(): + config.load() + assert config.config.get("user") == "Skywalker" + assert config.config.get("plugins") == {} + + +def test_save(): + config.set("user", "Yoda") + config.save() + config.load() + assert config.get("user") == "Yoda" + config.set("user", "Padawan") + config.save() + config.load() + assert config.get("user") == "Padawan" + + # assert that the config file is created + assert config.config_file.exists()