Skip to content

Commit 90c9c40

Browse files
Merge pull request #4 from martinmoldrup/features/add-a-plugin-system
Enhance CLI with plugin system and error handling
2 parents ca85516 + 31bca38 commit 90c9c40

File tree

6 files changed

+57
-8
lines changed

6 files changed

+57
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
66
## [0.3.0] - 09-09-2025
77
- Added toolit `create-vscode-tasks-json` CLI command to create the `.vscode/tasks.json` file from the command line.
88
- Improved error handling and user feedback when the `devtools` folder does not exist.
9+
- Added a plugin system to all for loading tools automatically from installed packages.
910

1011
## [0.2.0] - 16-06-2025
1112
- Fix problem with subdependencies not being installed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,15 @@ def my_parallel_commands() -> list[Callable]:
7171

7272
This will create a group of commands in the `tasks.json` file that can be executed sequentially or in parallel.
7373

74+
## Creating Plugins
75+
Toolit supports a plugin system that allows you to create and share your own tools as separate packages. This makes it easy to reuse tools across different projects, without needing to copy and update tools across multiple codebases.
76+
77+
To create a plugin, follow these steps:
78+
1. Create a new Python package for your plugin. You can use tools like `setuptools`, `poetry` or `uv` to set up your package structure.
79+
2. In your package, create a module where you define your tools using the `@tool` decorator.
80+
3. Make sure to include `toolit` as a dependency in your package's `setup.py` or `pyproject.toml`.
81+
4. Register your plugin with Toolit by adding an entry point in your `setup.py` or `pyproject.toml`, so Toolit can discover your tools when the package is installed. The entry point is called `toolit_plugins`.
82+
5. Publish your package to PyPI or install it from a git repository where you need it.
83+
7484
## Contributing
7585
We welcome contributions to Toolit! If you have ideas for new features, improvements, or bug fixes, please open an issue or submit a pull request on our GitHub repository. We appreciate your feedback and support in making Toolit even better for the community.

devtools/create_new_release.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Create a new release for the project, by reading the version from the pyproject.toml file, and adding and pushing a new tag to the repository."""
2+
23
import os
34
import subprocess
45
import toml
@@ -11,20 +12,23 @@
1112
CHANGELOG_MD = pathlib.Path(__file__).parent.parent / "CHANGELOG.md"
1213
PYPI_ENDPOINT = "https://pypi.org/pypi/toolit/json"
1314

15+
1416
def read_version():
1517
"""Read the version from the pyproject.toml file."""
1618
with open(PYPROJECT_TOML, "r", encoding="utf-8") as file:
1719
data = toml.load(file)
1820
return data["project"]["version"]
1921

22+
2023
def check_version(version: str):
2124
"""Check if the version is newer than the one in pypi."""
2225
response = requests.get(PYPI_ENDPOINT)
2326
pypi_version = response.json()["info"]["version"]
2427
print(f"Version in pypi: {pypi_version}")
2528
if Version(version) <= Version(pypi_version):
2629
raise ValueError(f"Version {version} is not newer than the one in pypi: {pypi_version}")
27-
30+
31+
2832
def check_change_log(version: str):
2933
"""Check if the version has a corresponding entry in the change log. It will be a line starting with ## [0.0.2]"""
3034
with open(CHANGELOG_MD, "r", encoding="utf-8") as file:
@@ -34,6 +38,7 @@ def check_change_log(version: str):
3438
raise ValueError(f"Version {version} does not have a corresponding entry in the change log.")
3539
print(f"Version {version} has a corresponding entry in the change log.")
3640

41+
3742
@tool
3843
def create_new_release():
3944
"""Create a new release for the project, by reading the version from the pyproject.toml file, and adding and pushing a new tag to the repository."""
@@ -60,5 +65,6 @@ def create_new_release():
6065

6166
print("Release created successfully.")
6267

68+
6369
if __name__ == "__main__":
64-
create_new_release()
70+
create_new_release()

toolit/auto_loader.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
import inspect
1313
import pathlib
1414
import importlib
15+
import importlib.metadata
1516
from toolit.constants import MARKER_TOOL, ToolitTypesEnum
1617
from toolit.create_apps_and_register import register_command
1718
from types import FunctionType, ModuleType
18-
from typing import Callable
19+
from typing import Any, Callable
1920

2021

2122
def get_items_from_folder(
@@ -51,6 +52,14 @@ def tool_group_strategy(module: ModuleType) -> list[FunctionType]:
5152
return groups
5253

5354

55+
def load_tools_from_plugins() -> list[FunctionType]:
56+
"""Discover and return plugin commands via entry points."""
57+
plugins = get_plugin_tools()
58+
for plugin in plugins:
59+
register_command(plugin)
60+
return plugins
61+
62+
5463
def load_tools_from_folder(folder_path: pathlib.Path) -> list[FunctionType]:
5564
"""
5665
Load all tools from a given folder and register them as commands.
@@ -108,3 +117,12 @@ def import_module(file: pathlib.Path) -> ModuleType:
108117
module_import_name = module_name
109118
module = importlib.import_module(module_import_name)
110119
return module
120+
121+
122+
def get_plugin_tools() -> list[FunctionType]:
123+
"""Discover and return plugin commands via entry points."""
124+
plugins = []
125+
for entry_point in importlib.metadata.entry_points().get("toolit_plugins", []):
126+
plugin_func: Any = entry_point.load()
127+
plugins.append(plugin_func)
128+
return plugins

toolit/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""CLI entry point for the toolit package."""
22
import pathlib
3-
from .auto_loader import load_tools_from_folder, register_command
3+
from .auto_loader import load_tools_from_folder, load_tools_from_plugins, register_command
44
from .create_apps_and_register import app
55
from .create_tasks_json import create_vscode_tasks_json
66

77
PATH = pathlib.Path() / "devtools"
88
load_tools_from_folder(PATH)
9+
load_tools_from_plugins()
910
register_command(create_vscode_tasks_json)
1011

12+
1113
if __name__ == "__main__":
1214
# Run the typer app
1315
app()

toolit/create_tasks_json.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
import typer
66
import inspect
77
import pathlib
8-
from toolit.auto_loader import get_items_from_folder, get_toolit_type, tool_group_strategy, tool_strategy
8+
from toolit.auto_loader import (
9+
get_items_from_folder,
10+
get_plugin_tools,
11+
get_toolit_type,
12+
tool_group_strategy,
13+
tool_strategy,
14+
)
915
from toolit.constants import ToolitTypesEnum
1016
from types import FunctionType
1117
from typing import Any
@@ -17,9 +23,15 @@
1723
def create_vscode_tasks_json() -> None:
1824
"""Create a tasks.json file based on the tools discovered in the project."""
1925
typer.echo(f"Creating tasks.json at {output_file_path}")
20-
tools: list[FunctionType] = get_items_from_folder(PATH, tool_strategy)
21-
tool_groups: list[FunctionType] = get_items_from_folder(PATH, tool_group_strategy)
22-
tools.extend(tool_groups)
26+
if PATH.exists() and PATH.is_dir():
27+
tools: list[FunctionType] = get_items_from_folder(PATH, tool_strategy)
28+
tool_groups: list[FunctionType] = get_items_from_folder(PATH, tool_group_strategy)
29+
tools.extend(tool_groups)
30+
else:
31+
typer.echo(f"The devtools folder does not exist or is not a directory: {PATH.absolute().as_posix()}")
32+
tools = []
33+
34+
tools.extend(get_plugin_tools())
2335
json_builder = TaskJsonBuilder()
2436
for tool in tools:
2537
json_builder.process_tool(tool)

0 commit comments

Comments
 (0)