From a3d76302192f03312afe5a87980a29cbf179a29b Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 15 Mar 2026 18:06:45 +0300 Subject: [PATCH] cli module better --- src/argenta/_cli/__main__.py | 31 +++++++-- src/argenta/_cli/commands/run.py | 9 +-- .../entrypoint_resolver/entity.py | 68 +++++++++---------- uv.lock | 33 ++++----- 4 files changed, 74 insertions(+), 67 deletions(-) diff --git a/src/argenta/_cli/__main__.py b/src/argenta/_cli/__main__.py index ec05f2c..20510f0 100644 --- a/src/argenta/_cli/__main__.py +++ b/src/argenta/_cli/__main__.py @@ -1,14 +1,33 @@ from typer import Typer -from .commands import run_handler, init_handler, new_handler +from .commands import init_handler, new_handler, run_handler def main() -> None: app = Typer() - app.command("run", help='Command to start the orchestrator repl; the path to the callable object is required')(run_handler) - app.command("init", help="Creates a flat/src boilerplate architecture in an existing project")(init_handler) - app.command("new", help="Creates a project and in it flat/src boilerplate architecture")(new_handler) + app.command( + "run", + help="Command to start the orchestrator repl; the path to the callable object is required", + short_help="Start the orchestrator REPL", + epilog="Example: run app/main.py:main", + )(run_handler) + + app.command( + "init", + help="Creates a flat/src boilerplate architecture in an existing project", + short_help="Initialize architecture in existing project", + epilog="Make sure you are in the project root before running this command.", + )(init_handler) + + app.command( + "new", + help="Creates a project and in it flat/src boilerplate architecture", + short_help="Create a new project with boilerplate", + epilog="This will create a new directory with the project structure.", + )(new_handler) + app() -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/src/argenta/_cli/commands/run.py b/src/argenta/_cli/commands/run.py index 2c46a2b..312d2f0 100644 --- a/src/argenta/_cli/commands/run.py +++ b/src/argenta/_cli/commands/run.py @@ -1,8 +1,6 @@ __all__ = ["run_handler"] import os -from pathlib import Path -import sys from ..infrastructure.entrypoint_resolver.entity import ( CallableEntryPoint, @@ -18,12 +16,9 @@ def run_handler(entrypoint_path: str) -> None: raise ResolveFromStringError( "Path to callable object that run orchestrator repl must be in the format :" ) - - if str(Path.cwd()) not in sys.path: - sys.path.insert(0, str(Path.cwd())) - runner = EntrypointResolver(entrypoint_path).parse_entrypoint_with_type( - entrypoint_callable_name, CallableEntryPoint + runner = EntrypointResolver[CallableEntryPoint](entrypoint_path).parse_entrypoint_with_type( + entrypoint_callable_name ) runner.instance_object() diff --git a/src/argenta/_cli/infrastructure/entrypoint_resolver/entity.py b/src/argenta/_cli/infrastructure/entrypoint_resolver/entity.py index ec91500..abfd458 100644 --- a/src/argenta/_cli/infrastructure/entrypoint_resolver/entity.py +++ b/src/argenta/_cli/infrastructure/entrypoint_resolver/entity.py @@ -1,10 +1,10 @@ __all__ = ['EntrypointResolver', 'EntryPointAsApp', 'CallableEntryPoint'] -import importlib.util import inspect from dataclasses import dataclass from pathlib import Path -from typing import Callable, Protocol, cast, overload +import sys +from typing import Callable, Protocol, cast, get_args from argenta.app.models import App @@ -35,28 +35,18 @@ class EntryPointAsApp: instance_object: App -class EntrypointResolver: +class EntrypointResolver[T: (CallableEntryPoint, EntryPointAsApp)]: def __init__(self, path_to_entrypoint: str): self._path_to_entrypoint = path_to_entrypoint - @overload def parse_entrypoint_with_type( - self, entrypoint_object_name: str, entrypoint_type: type[CallableEntryPoint] - ) -> EntryPoint[Callable[[], None]]: ... - @overload - def parse_entrypoint_with_type( - self, entrypoint_object_name: str, entrypoint_type: type[EntryPointAsApp] - ) -> EntryPoint[App]: ... - - def parse_entrypoint_with_type( - self, - entrypoint_object_name: str, - entrypoint_type: type[CallableEntryPoint] | type[EntryPointAsApp], - ) -> EntryPoint[Callable[[], None]] | EntryPoint[App]: + self, entrypoint_object_name: str, + ) -> T: + entrypoint_type: type[T] = get_args(self.__orig_class__)[0] # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType] if entrypoint_type is CallableEntryPoint: - return self._parse_callable_entrypoint(entrypoint_object_name) + return cast(T, self._parse_callable_entrypoint(entrypoint_object_name)) elif entrypoint_type is EntryPointAsApp: - return self._parse_entrypoint_as_app(entrypoint_object_name) + return cast(T, self._parse_entrypoint_as_app(entrypoint_object_name)) raise NotImplementedError def _parse_callable_entrypoint(self, entrypoint_object_name: str) -> CallableEntryPoint: @@ -82,23 +72,31 @@ class EntrypointResolver: return EntryPointAsApp(raw_path=resolved_entrypoint[0], instance_object=instance_object) def _resolve_from_string(self, entrypoint_object_name: str) -> tuple[str, object]: - file_path: str = self._path_to_entrypoint - attr_name: str = entrypoint_object_name - - path = Path(file_path).resolve() - if not path.exists(): - raise ResolveFromStringError(f'File "{file_path}" not found') - - spec = importlib.util.spec_from_file_location(path.stem, path) - if spec is None or spec.loader is None: - raise ResolveFromStringError(f'Cannot load module from "{file_path}"') - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - + abs_path = Path(self._path_to_entrypoint).resolve() + if not abs_path.exists(): + raise ResolveFromStringError(f'File "{self._path_to_entrypoint}" not found') + + package_root = abs_path.parent + while (package_root / "__init__.py").exists(): + package_root = package_root.parent + + pkg_root_str = str(package_root) + if pkg_root_str not in sys.path: + sys.path.insert(0, pkg_root_str) + + module_name = ".".join(abs_path.relative_to(package_root).with_suffix("").parts) + try: - instance = getattr(module, attr_name) + module = importlib.import_module(module_name) + except ImportError as e: + raise ResolveFromStringError(f'Cannot import module "{module_name}": {e}') + + try: + instance = getattr(module, entrypoint_object_name) except AttributeError: - raise ResolveFromStringError(f'"{attr_name}" not found in "{file_path}"') + raise ResolveFromStringError( + f'"{entrypoint_object_name}" not found in "{self._path_to_entrypoint}"' + ) + + return str(abs_path), instance - return file_path, instance diff --git a/uv.lock b/uv.lock index d18abea..16b28ff 100644 --- a/uv.lock +++ b/uv.lock @@ -55,10 +55,15 @@ cli = [ [package.dev-dependencies] dev = [ + { name = "cairosvg" }, { name = "esbonio" }, { name = "isort" }, + { name = "matplotlib" }, { name = "mypy" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, { name = "pyfakefs" }, + { name = "pygal" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-mock" }, @@ -77,20 +82,17 @@ docs = [ { name = "sphinx-autobuild" }, { name = "sphinx-intl" }, ] -in-test = [ - { name = "cairosvg" }, - { name = "plotext" }, - { name = "pygal" }, -] linters = [ { name = "isort" }, { name = "ruff" }, { name = "wemake-python-styleguide" }, ] metrics = [ + { name = "cairosvg" }, { name = "matplotlib" }, { name = "psutil" }, { name = "py-cpuinfo" }, + { name = "pygal" }, ] tests = [ { name = "pyfakefs" }, @@ -114,10 +116,15 @@ provides-extras = ["cli"] [package.metadata.requires-dev] dev = [ + { name = "cairosvg", specifier = ">=2.8.2" }, { name = "esbonio", specifier = ">=1.0.0" }, { name = "isort", specifier = ">=7.0.0" }, + { name = "matplotlib", specifier = ">=3.10.8" }, { name = "mypy", specifier = ">=1.14.1" }, + { name = "psutil", specifier = ">=7.2.1" }, + { name = "py-cpuinfo", specifier = ">=9.0.0" }, { name = "pyfakefs", specifier = ">=5.5.0" }, + { name = "pygal", specifier = ">=3.1.0" }, { name = "pytest", specifier = ">=8.3.2" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, @@ -136,20 +143,17 @@ docs = [ { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, { name = "sphinx-intl", specifier = ">=2.3.2" }, ] -in-test = [ - { name = "cairosvg", specifier = ">=2.8.2" }, - { name = "plotext", specifier = ">=5.3.2" }, - { name = "pygal", specifier = ">=3.1.0" }, -] linters = [ { name = "isort", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.12.12" }, { name = "wemake-python-styleguide", specifier = ">=0.17.0" }, ] metrics = [ + { name = "cairosvg", specifier = ">=2.8.2" }, { name = "matplotlib", specifier = ">=3.10.8" }, { name = "psutil", specifier = ">=7.2.1" }, { name = "py-cpuinfo", specifier = ">=9.0.0" }, + { name = "pygal", specifier = ">=3.1.0" }, ] tests = [ { name = "pyfakefs", specifier = ">=5.5.0" }, @@ -1142,15 +1146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, ] -[[package]] -name = "plotext" -version = "5.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz", hash = "sha256:52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e", size = 61967, upload-time = "2024-09-24T15:13:37.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/1e/12fe7c40cd2099a1f454518754ed229b01beaf3bbb343127f0cc13ce6c22/plotext-5.3.2-py3-none-any.whl", hash = "sha256:394362349c1ddbf319548cfac17ca65e6d5dfc03200c40dfdc0503b3e95a2283", size = 64047, upload-time = "2024-09-24T15:13:36.296Z" }, -] - [[package]] name = "pluggy" version = "1.6.0"