Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/continuous_integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ jobs:
lint_and_type_check:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.14"]

steps:
- name: Checkout repository
uses: actions/checkout@v2
Expand Down Expand Up @@ -42,7 +47,9 @@ jobs:
python-version: ["3.10", "3.13"]
include-extras: [true]
include:
- python-version: "3.9"
- python-version: "3.10"
include-extras: false
- python-version: "3.14"
include-extras: false

steps:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

*NOTE:* Version 0.X.X might have breaking changes in bumps of the minor version number. This is because the project is still in early development and the API is not yet stable. It will still be marked clearly in the release notes.

## [0.6.0] - 20-12-2025
- Abandon python 3.9 which is deprecated. Now only support python 3.10 and higher.
- Fix bug with plugin system, in newer python version, where it would raise an exception when loading plugins.

## [0.5.0] - 24-09-2025
- Improvements to the plugin system. Plugins can choose their own names and the plugins is show in a seperate section in the rich help panel.

Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[project]
name = "toolit"
version = "0.5.0"
version = "0.6.0"
description = "MCP Server, Typer CLI and vscode tasks in one, provides an easy way to configure your own DevTools and python scripts in a project."
readme = "README.md"
requires-python = ">=3.9" # mcp[cli] requires Python 3.10+
requires-python = ">=3.10"
dependencies = [
"toml",
"typer",
Expand All @@ -15,7 +15,6 @@ classifiers = [
"Topic :: Software Development :: Testing",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down Expand Up @@ -49,7 +48,7 @@ dev = [
]

[project.optional-dependencies]
mcp = ["mcp[cli]; python_version >= '3.10'"]
mcp = ["mcp[cli]"]

[project.scripts]
toolit = "toolit.cli:app"
Expand Down
4 changes: 2 additions & 2 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
line-length = 120
indent-width = 4

# Assume Python 3.9
target-version = "py39"
# Assume Python 3.10+ codebase
target-version = "py310"
output-format = "concise"

[lint]
Expand Down
17 changes: 17 additions & 0 deletions tests/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
from typer.testing import CliRunner


def test_cli_run_with_no_tools() -> None:
# Get the commands from the typer cli app
runner = CliRunner()
result = runner.invoke(create_apps_and_register.app, ["--help"])
assert result.exit_code == 0
print(result.stdout)


def test_cli_loads_tools_and_plugins_without_plugins() -> None:
from toolit.register_all_tool_and_plugins import register_all_tools_from_folder_and_plugin
register_all_tools_from_folder_and_plugin()
runner = CliRunner()
result = runner.invoke(create_apps_and_register.app, ["--help"])
assert result.exit_code == 0

def a_new_command_registered() -> None:
"""Test a new command is registered."""
print("This is a new command registered.")
Expand All @@ -14,3 +29,5 @@ def test_cli_command_is_registered() -> None:
result = runner.invoke(create_apps_and_register.app, ["--help"])
assert result.exit_code == 0
assert "a-new-command-registered" in result.stdout


17 changes: 13 additions & 4 deletions toolit/auto_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
A folder is defined. Everything that has the @decorators.tool decorator will be loaded
and added as CLI and MCP commands.
"""

from __future__ import annotations

import os
Expand All @@ -13,10 +14,11 @@
import pathlib
import importlib
import importlib.metadata
from collections.abc import Callable
from toolit.constants import MARKER_TOOL, RichHelpPanelNames, ToolitTypesEnum
from toolit.create_apps_and_register import register_command
from types import FunctionType, ModuleType
from typing import Any, Callable
from typing import Any


def get_items_from_folder(
Expand Down Expand Up @@ -96,7 +98,7 @@ def get_toolit_type(tool: FunctionType) -> ToolitTypesEnum | None:

def load_tools_from_file(module: ModuleType, tool_type: ToolitTypesEnum) -> list[FunctionType]:
"""Load a tool from a given file and register it as a command."""
tools = []
tools: list[FunctionType] = []
for _name, obj in inspect.getmembers(module):
is_tool: bool = get_toolit_type(obj) == tool_type
if inspect.isfunction(obj) and is_tool:
Expand All @@ -119,10 +121,17 @@ def import_module(file: pathlib.Path) -> ModuleType:
return module


def get_entry_points(name: str) -> importlib.metadata.EntryPoints:
"""Get entry points by group name."""
entry_points = importlib.metadata.entry_points()
return entry_points.select(group=name)


def get_plugin_tools() -> list[FunctionType]:
"""Discover and return plugin commands via entry points."""
plugins = []
for entry_point in importlib.metadata.entry_points().get("toolit_plugins", []):
plugins: list[FunctionType] = []
entry_points = get_entry_points("toolit_plugins")
for entry_point in entry_points:
plugin_func: Any = entry_point.load()
plugin_func.__name__ = entry_point.name
plugins.append(plugin_func)
Expand Down
10 changes: 2 additions & 8 deletions toolit/cli.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
"""CLI entry point for the toolit package."""
from toolit.auto_loader import load_tools_from_folder, load_tools_from_plugins, register_command
from toolit.config import load_devtools_folder
from toolit.constants import RichHelpPanelNames
from toolit.create_apps_and_register import app
from toolit.create_tasks_json import create_vscode_tasks_json

load_tools_from_folder(load_devtools_folder())
load_tools_from_plugins()
register_command(create_vscode_tasks_json, rich_help_panel=RichHelpPanelNames.PLUGINS_COMMANDS_PANEL)
from toolit.register_all_tool_and_plugins import register_all_tools_from_folder_and_plugin

register_all_tools_from_folder_and_plugin()

if __name__ == "__main__":
# Run the typer app
Expand Down
3 changes: 2 additions & 1 deletion toolit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@

import toml
import pathlib
from collections.abc import Callable
from functools import lru_cache
from toolit.constants import ConfigFileKeys
from typing import Callable, overload
from typing import overload


def load_ini_config(file_path: pathlib.Path) -> dict[str, str]:
Expand Down
3 changes: 2 additions & 1 deletion toolit/decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Decorator to tell if a function is a tool."""

from collections.abc import Callable
from toolit.constants import MARKER_TOOL, ToolitTypesEnum
from typing import Any, Callable, TypeVar
from typing import Any, TypeVar

T = TypeVar("T", bound=Callable[..., Any])

Expand Down
13 changes: 13 additions & 0 deletions toolit/register_all_tool_and_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Register all tools and plugins to be used by default."""
from toolit.auto_loader import load_tools_from_folder, load_tools_from_plugins
from toolit.config import load_devtools_folder
from toolit.constants import RichHelpPanelNames
from toolit.create_apps_and_register import register_command
from toolit.create_tasks_json import create_vscode_tasks_json


def register_all_tools_from_folder_and_plugin() -> None:
"""Load and register all tools that will be used by default."""
load_tools_from_folder(load_devtools_folder())
load_tools_from_plugins()
register_command(create_vscode_tasks_json, rich_help_panel=RichHelpPanelNames.PLUGINS_COMMANDS_PANEL)
Loading