feat: allow custom CLI (#28)

This commit is contained in:
d3vyce
2026-02-04 17:19:07 +01:00
committed by GitHub
parent 1ff94eb9d3
commit e0bc93096d
7 changed files with 476 additions and 212 deletions

View File

@@ -1,52 +1,28 @@
"""CLI configuration."""
"""CLI configuration and dynamic imports."""
import importlib
import sys
import tomllib
from dataclasses import dataclass
from pathlib import Path
import typer
@dataclass
class CliConfig:
"""CLI configuration loaded from pyproject.toml."""
fixtures: str | None = None
db_context: str | None = None
def get_fixtures_registry(self):
"""Import and return the fixtures registry."""
from ..fixtures import FixtureRegistry
if not self.fixtures:
raise typer.BadParameter(
"No fixtures registry configured. "
"Add 'fixtures' to [tool.fastapi-toolsets] in pyproject.toml."
)
registry = _import_from_string(self.fixtures)
if not isinstance(registry, FixtureRegistry):
raise typer.BadParameter(
f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
)
return registry
def get_db_context(self):
"""Import and return the db_context function."""
if not self.db_context:
raise typer.BadParameter(
"No db_context configured. "
"Add 'db_context' to [tool.fastapi-toolsets] in pyproject.toml."
)
return _import_from_string(self.db_context)
from .pyproject import find_pyproject, load_pyproject
def _import_from_string(import_path: str):
"""Import an object from a string path like 'module.submodule:attribute'."""
def _ensure_project_in_path():
"""Add project root to sys.path if not installed in editable mode."""
pyproject = find_pyproject()
if pyproject:
project_root = str(pyproject.parent)
if project_root not in sys.path:
sys.path.insert(0, project_root)
def import_from_string(import_path: str):
"""Import an object from a string path like 'module.submodule:attribute'.
Raises:
typer.BadParameter: If the import path is invalid or import fails.
"""
if ":" not in import_path:
raise typer.BadParameter(
f"Invalid import path '{import_path}'. Expected format: 'module:attribute'"
@@ -54,10 +30,7 @@ def _import_from_string(import_path: str):
module_path, attr_name = import_path.rsplit(":", 1)
# Add cwd to sys.path for local imports
cwd = str(Path.cwd())
if cwd not in sys.path:
sys.path.insert(0, cwd)
_ensure_project_in_path()
try:
module = importlib.import_module(module_path)
@@ -72,21 +45,63 @@ def _import_from_string(import_path: str):
return getattr(module, attr_name)
def load_config() -> CliConfig:
"""Load CLI configuration from pyproject.toml."""
pyproject_path = Path.cwd() / "pyproject.toml"
def get_config_value(key: str, required: bool = False):
"""Get a configuration value from pyproject.toml.
if not pyproject_path.exists():
return CliConfig()
Args:
key: The configuration key in [tool.fastapi-toolsets].
required: If True, raises an error when the key is missing.
try:
with open(pyproject_path, "rb") as f:
data = tomllib.load(f)
Returns:
The configuration value, or None if not found and not required.
tool_config = data.get("tool", {}).get("fastapi-toolsets", {})
return CliConfig(
fixtures=tool_config.get("fixtures"),
db_context=tool_config.get("db_context"),
Raises:
typer.BadParameter: If required=True and the key is missing.
"""
config = load_pyproject()
value = config.get(key)
if required and value is None:
raise typer.BadParameter(
f"No '{key}' configured. "
f"Add '{key}' to [tool.fastapi-toolsets] in pyproject.toml."
)
except Exception:
return CliConfig()
return value
def get_fixtures_registry():
"""Import and return the fixtures registry from config."""
from ..fixtures import FixtureRegistry
import_path = get_config_value("fixtures", required=True)
registry = import_from_string(import_path)
if not isinstance(registry, FixtureRegistry):
raise typer.BadParameter(
f"'fixtures' must be a FixtureRegistry instance, got {type(registry).__name__}"
)
return registry
def get_db_context():
"""Import and return the db_context function from config."""
import_path = get_config_value("db_context", required=True)
return import_from_string(import_path)
def get_custom_cli() -> typer.Typer | None:
"""Import and return the custom CLI Typer instance from config."""
import_path = get_config_value("custom_cli")
if not import_path:
return None
custom = import_from_string(import_path)
if not isinstance(custom, typer.Typer):
raise typer.BadParameter(
f"'custom_cli' must be a Typer instance, got {type(custom).__name__}"
)
return custom