Add LoadFileAction, Rename ActionFactoryAction->ActionFactory, Rename falyx.action.mixins->falyx.action.action_mixins, fix bug unable to parse negative numbers in CommandArgumentParser
This commit is contained in:
@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from falyx import Falyx
|
from falyx import Falyx
|
||||||
from falyx.action import ActionFactoryAction, ChainedAction, HTTPAction, SelectionAction
|
from falyx.action import ActionFactory, ChainedAction, HTTPAction, SelectionAction
|
||||||
|
|
||||||
# Selection of a post ID to fetch (just an example set)
|
# Selection of a post ID to fetch (just an example set)
|
||||||
post_selector = SelectionAction(
|
post_selector = SelectionAction(
|
||||||
@ -24,7 +24,7 @@ async def build_post_action(post_id) -> HTTPAction:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
post_factory = ActionFactoryAction(
|
post_factory = ActionFactory(
|
||||||
name="Build HTTPAction from Post ID",
|
name="Build HTTPAction from Post ID",
|
||||||
factory=build_post_action,
|
factory=build_post_action,
|
||||||
inject_last_result=True,
|
inject_last_result=True,
|
||||||
|
@ -29,7 +29,10 @@ flx.add_command(
|
|||||||
),
|
),
|
||||||
arg_metadata={
|
arg_metadata={
|
||||||
"service": "Service name",
|
"service": "Service name",
|
||||||
"region": {"help": "Deployment region", "choices": ["us-east-1", "us-west-2"]},
|
"region": {
|
||||||
|
"help": "Deployment region",
|
||||||
|
"choices": ["us-east-1", "us-west-2", "eu-west-1"],
|
||||||
|
},
|
||||||
"verbose": {"help": "Enable verbose mode"},
|
"verbose": {"help": "Enable verbose mode"},
|
||||||
},
|
},
|
||||||
tags=["deployment", "service"],
|
tags=["deployment", "service"],
|
||||||
|
@ -6,7 +6,7 @@ Licensed under the MIT License. See LICENSE file for details.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .action import Action
|
from .action import Action
|
||||||
from .action_factory import ActionFactoryAction
|
from .action_factory import ActionFactory
|
||||||
from .action_group import ActionGroup
|
from .action_group import ActionGroup
|
||||||
from .base_action import BaseAction
|
from .base_action import BaseAction
|
||||||
from .chained_action import ChainedAction
|
from .chained_action import ChainedAction
|
||||||
@ -14,6 +14,7 @@ from .fallback_action import FallbackAction
|
|||||||
from .http_action import HTTPAction
|
from .http_action import HTTPAction
|
||||||
from .io_action import BaseIOAction
|
from .io_action import BaseIOAction
|
||||||
from .literal_input_action import LiteralInputAction
|
from .literal_input_action import LiteralInputAction
|
||||||
|
from .load_file_action import LoadFileAction
|
||||||
from .menu_action import MenuAction
|
from .menu_action import MenuAction
|
||||||
from .process_action import ProcessAction
|
from .process_action import ProcessAction
|
||||||
from .process_pool_action import ProcessPoolAction
|
from .process_pool_action import ProcessPoolAction
|
||||||
@ -30,7 +31,7 @@ __all__ = [
|
|||||||
"BaseAction",
|
"BaseAction",
|
||||||
"ChainedAction",
|
"ChainedAction",
|
||||||
"ProcessAction",
|
"ProcessAction",
|
||||||
"ActionFactoryAction",
|
"ActionFactory",
|
||||||
"HTTPAction",
|
"HTTPAction",
|
||||||
"BaseIOAction",
|
"BaseIOAction",
|
||||||
"ShellAction",
|
"ShellAction",
|
||||||
@ -43,4 +44,5 @@ __all__ = [
|
|||||||
"UserInputAction",
|
"UserInputAction",
|
||||||
"PromptMenuAction",
|
"PromptMenuAction",
|
||||||
"ProcessPoolAction",
|
"ProcessPoolAction",
|
||||||
|
"LoadFileAction",
|
||||||
]
|
]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"""action.py"""
|
"""action.py"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable
|
from typing import Any, Awaitable, Callable
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
@ -42,9 +42,9 @@ class Action(BaseAction):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
action: Callable[..., Any],
|
action: Callable[..., Any] | Callable[..., Awaitable[Any]],
|
||||||
*,
|
*,
|
||||||
rollback: Callable[..., Any] | None = None,
|
rollback: Callable[..., Any] | Callable[..., Awaitable[Any]] | None = None,
|
||||||
args: tuple[Any, ...] = (),
|
args: tuple[Any, ...] = (),
|
||||||
kwargs: dict[str, Any] | None = None,
|
kwargs: dict[str, Any] | None = None,
|
||||||
hooks: HookManager | None = None,
|
hooks: HookManager | None = None,
|
||||||
@ -69,19 +69,19 @@ class Action(BaseAction):
|
|||||||
self.enable_retry()
|
self.enable_retry()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def action(self) -> Callable[..., Any]:
|
def action(self) -> Callable[..., Awaitable[Any]]:
|
||||||
return self._action
|
return self._action
|
||||||
|
|
||||||
@action.setter
|
@action.setter
|
||||||
def action(self, value: Callable[..., Any]):
|
def action(self, value: Callable[..., Awaitable[Any]]):
|
||||||
self._action = ensure_async(value)
|
self._action = ensure_async(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rollback(self) -> Callable[..., Any] | None:
|
def rollback(self) -> Callable[..., Awaitable[Any]] | None:
|
||||||
return self._rollback
|
return self._rollback
|
||||||
|
|
||||||
@rollback.setter
|
@rollback.setter
|
||||||
def rollback(self, value: Callable[..., Any] | None):
|
def rollback(self, value: Callable[..., Awaitable[Any]] | None):
|
||||||
if value is None:
|
if value is None:
|
||||||
self._rollback = None
|
self._rollback = None
|
||||||
else:
|
else:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""action_factory.py"""
|
"""action_factory_action.py"""
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
@ -14,7 +14,7 @@ from falyx.themes import OneColors
|
|||||||
from falyx.utils import ensure_async
|
from falyx.utils import ensure_async
|
||||||
|
|
||||||
|
|
||||||
class ActionFactoryAction(BaseAction):
|
class ActionFactory(BaseAction):
|
||||||
"""
|
"""
|
||||||
Dynamically creates and runs another Action at runtime using a factory function.
|
Dynamically creates and runs another Action at runtime using a factory function.
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
"""action_group.py"""
|
"""action_group.py"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
from typing import Any, Callable, Sequence
|
from typing import Any, Awaitable, Callable, Sequence
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
|
from falyx.action.action_mixins import ActionListMixin
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
from falyx.action.mixins import ActionListMixin
|
|
||||||
from falyx.context import ExecutionContext, SharedContext
|
from falyx.context import ExecutionContext, SharedContext
|
||||||
|
from falyx.exceptions import EmptyGroupError
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import Hook, HookManager, HookType
|
from falyx.hook_manager import Hook, HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
@ -54,7 +55,9 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
|
actions: (
|
||||||
|
Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable]] | None
|
||||||
|
) = None,
|
||||||
*,
|
*,
|
||||||
hooks: HookManager | None = None,
|
hooks: HookManager | None = None,
|
||||||
inject_last_result: bool = False,
|
inject_last_result: bool = False,
|
||||||
@ -104,6 +107,8 @@ class ActionGroup(BaseAction, ActionListMixin):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
|
||||||
|
if not self.actions:
|
||||||
|
raise EmptyGroupError(f"[{self.name}] No actions to execute.")
|
||||||
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
||||||
if self.shared_context:
|
if self.shared_context:
|
||||||
shared_context.set_shared_result(self.shared_context.last_result())
|
shared_context.set_shared_result(self.shared_context.last_result())
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
"""mixins.py"""
|
"""action_mixins.py"""
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
@ -38,7 +38,6 @@ from rich.tree import Tree
|
|||||||
|
|
||||||
from falyx.context import SharedContext
|
from falyx.context import SharedContext
|
||||||
from falyx.debug import register_debug_hooks
|
from falyx.debug import register_debug_hooks
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
|
||||||
from falyx.hook_manager import Hook, HookManager, HookType
|
from falyx.hook_manager import Hook, HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
from falyx.options_manager import OptionsManager
|
from falyx.options_manager import OptionsManager
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
"""chained_action.py"""
|
"""chained_action.py"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Callable, Sequence
|
from typing import Any, Awaitable, Callable, Sequence
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action.action import Action
|
from falyx.action.action import Action
|
||||||
|
from falyx.action.action_mixins import ActionListMixin
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
from falyx.action.fallback_action import FallbackAction
|
from falyx.action.fallback_action import FallbackAction
|
||||||
from falyx.action.literal_input_action import LiteralInputAction
|
from falyx.action.literal_input_action import LiteralInputAction
|
||||||
from falyx.action.mixins import ActionListMixin
|
|
||||||
from falyx.context import ExecutionContext, SharedContext
|
from falyx.context import ExecutionContext, SharedContext
|
||||||
from falyx.exceptions import EmptyChainError
|
from falyx.exceptions import EmptyChainError
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
@ -47,7 +47,10 @@ class ChainedAction(BaseAction, ActionListMixin):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
|
actions: (
|
||||||
|
Sequence[BaseAction | Callable[..., Any] | Callable[..., Awaitable[Any]]]
|
||||||
|
| None
|
||||||
|
) = None,
|
||||||
*,
|
*,
|
||||||
hooks: HookManager | None = None,
|
hooks: HookManager | None = None,
|
||||||
inject_last_result: bool = False,
|
inject_last_result: bool = False,
|
||||||
|
196
falyx/action/load_file_action.py
Normal file
196
falyx/action/load_file_action.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
|
||||||
|
"""load_file_action.py"""
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import toml
|
||||||
|
import yaml
|
||||||
|
from rich.tree import Tree
|
||||||
|
|
||||||
|
from falyx.action.action_types import FileType
|
||||||
|
from falyx.action.base_action import BaseAction
|
||||||
|
from falyx.context import ExecutionContext
|
||||||
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
|
from falyx.hook_manager import HookType
|
||||||
|
from falyx.logger import logger
|
||||||
|
from falyx.themes import OneColors
|
||||||
|
|
||||||
|
|
||||||
|
class LoadFileAction(BaseAction):
|
||||||
|
"""LoadFileAction allows loading and parsing files of various types."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
file_path: str | Path | None = None,
|
||||||
|
file_type: FileType | str = FileType.TEXT,
|
||||||
|
inject_last_result: bool = False,
|
||||||
|
inject_into: str = "file_path",
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
name=name, inject_last_result=inject_last_result, inject_into=inject_into
|
||||||
|
)
|
||||||
|
self._file_path = self._coerce_file_path(file_path)
|
||||||
|
self._file_type = self._coerce_file_type(file_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_path(self) -> Path | None:
|
||||||
|
"""Get the file path as a Path object."""
|
||||||
|
return self._file_path
|
||||||
|
|
||||||
|
@file_path.setter
|
||||||
|
def file_path(self, value: str | Path):
|
||||||
|
"""Set the file path, converting to Path if necessary."""
|
||||||
|
self._file_path = self._coerce_file_path(value)
|
||||||
|
|
||||||
|
def _coerce_file_path(self, file_path: str | Path | None) -> Path | None:
|
||||||
|
"""Coerce the file path to a Path object."""
|
||||||
|
if isinstance(file_path, Path):
|
||||||
|
return file_path
|
||||||
|
elif isinstance(file_path, str):
|
||||||
|
return Path(file_path)
|
||||||
|
elif file_path is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise TypeError("file_path must be a string or Path object")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def file_type(self) -> FileType:
|
||||||
|
"""Get the file type."""
|
||||||
|
return self._file_type
|
||||||
|
|
||||||
|
@file_type.setter
|
||||||
|
def file_type(self, value: FileType | str):
|
||||||
|
"""Set the file type, converting to FileType if necessary."""
|
||||||
|
self._file_type = self._coerce_file_type(value)
|
||||||
|
|
||||||
|
def _coerce_file_type(self, file_type: FileType | str) -> FileType:
|
||||||
|
"""Coerce the file type to a FileType enum."""
|
||||||
|
if isinstance(file_type, FileType):
|
||||||
|
return file_type
|
||||||
|
elif isinstance(file_type, str):
|
||||||
|
return FileType(file_type)
|
||||||
|
else:
|
||||||
|
raise TypeError("file_type must be a FileType enum or string")
|
||||||
|
|
||||||
|
def get_infer_target(self) -> tuple[None, None]:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def load_file(self) -> Any:
|
||||||
|
if self.file_path is None:
|
||||||
|
raise ValueError("file_path must be set before loading a file")
|
||||||
|
value: Any = None
|
||||||
|
try:
|
||||||
|
if self.file_type == FileType.TEXT:
|
||||||
|
value = self.file_path.read_text(encoding="UTF-8")
|
||||||
|
elif self.file_type == FileType.PATH:
|
||||||
|
value = self.file_path
|
||||||
|
elif self.file_type == FileType.JSON:
|
||||||
|
value = json.loads(self.file_path.read_text(encoding="UTF-8"))
|
||||||
|
elif self.file_type == FileType.TOML:
|
||||||
|
value = toml.loads(self.file_path.read_text(encoding="UTF-8"))
|
||||||
|
elif self.file_type == FileType.YAML:
|
||||||
|
value = yaml.safe_load(self.file_path.read_text(encoding="UTF-8"))
|
||||||
|
elif self.file_type == FileType.CSV:
|
||||||
|
with open(self.file_path, newline="", encoding="UTF-8") as csvfile:
|
||||||
|
reader = csv.reader(csvfile)
|
||||||
|
value = list(reader)
|
||||||
|
elif self.file_type == FileType.TSV:
|
||||||
|
with open(self.file_path, newline="", encoding="UTF-8") as tsvfile:
|
||||||
|
reader = csv.reader(tsvfile, delimiter="\t")
|
||||||
|
value = list(reader)
|
||||||
|
elif self.file_type == FileType.XML:
|
||||||
|
tree = ET.parse(self.file_path, parser=ET.XMLParser(encoding="UTF-8"))
|
||||||
|
root = tree.getroot()
|
||||||
|
value = ET.tostring(root, encoding="unicode")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported return type: {self.file_type}")
|
||||||
|
|
||||||
|
except Exception as error:
|
||||||
|
logger.error("Failed to parse %s: %s", self.file_path.name, error)
|
||||||
|
return value
|
||||||
|
|
||||||
|
async def _run(self, *args, **kwargs) -> Any:
|
||||||
|
context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
|
||||||
|
context.start_timer()
|
||||||
|
try:
|
||||||
|
await self.hooks.trigger(HookType.BEFORE, context)
|
||||||
|
|
||||||
|
if "file_path" in kwargs:
|
||||||
|
self.file_path = kwargs["file_path"]
|
||||||
|
elif self.inject_last_result and self.last_result:
|
||||||
|
self.file_path = self.last_result
|
||||||
|
|
||||||
|
if self.file_path is None:
|
||||||
|
raise ValueError("file_path must be set before loading a file")
|
||||||
|
elif not self.file_path.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {self.file_path}")
|
||||||
|
elif not self.file_path.is_file():
|
||||||
|
raise ValueError(f"Path is not a regular file: {self.file_path}")
|
||||||
|
|
||||||
|
result = self.load_file()
|
||||||
|
await self.hooks.trigger(HookType.ON_SUCCESS, context)
|
||||||
|
return result
|
||||||
|
except Exception as error:
|
||||||
|
context.exception = error
|
||||||
|
await self.hooks.trigger(HookType.ON_ERROR, context)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
context.stop_timer()
|
||||||
|
await self.hooks.trigger(HookType.AFTER, context)
|
||||||
|
await self.hooks.trigger(HookType.ON_TEARDOWN, context)
|
||||||
|
er.record(context)
|
||||||
|
|
||||||
|
async def preview(self, parent: Tree | None = None):
|
||||||
|
label = f"[{OneColors.GREEN}]📄 LoadFileAction[/] '{self.name}'"
|
||||||
|
tree = parent.add(label) if parent else Tree(label)
|
||||||
|
|
||||||
|
tree.add(f"[dim]Path:[/] {self.file_path}")
|
||||||
|
tree.add(f"[dim]Type:[/] {self.file_type.name if self.file_type else 'None'}")
|
||||||
|
if self.file_path is None:
|
||||||
|
tree.add(f"[{OneColors.DARK_RED_b}]❌ File path is not set[/]")
|
||||||
|
elif not self.file_path.exists():
|
||||||
|
tree.add(f"[{OneColors.DARK_RED_b}]❌ File does not exist[/]")
|
||||||
|
elif not self.file_path.is_file():
|
||||||
|
tree.add(f"[{OneColors.LIGHT_YELLOW_b}]⚠️ Not a regular file[/]")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
stat = self.file_path.stat()
|
||||||
|
tree.add(f"[dim]Size:[/] {stat.st_size:,} bytes")
|
||||||
|
tree.add(
|
||||||
|
f"[dim]Modified:[/] {datetime.fromtimestamp(stat.st_mtime):%Y-%m-%d %H:%M:%S}"
|
||||||
|
)
|
||||||
|
tree.add(
|
||||||
|
f"[dim]Created:[/] {datetime.fromtimestamp(stat.st_ctime):%Y-%m-%d %H:%M:%S}"
|
||||||
|
)
|
||||||
|
if self.file_type == FileType.TEXT:
|
||||||
|
preview_lines = self.file_path.read_text(
|
||||||
|
encoding="UTF-8"
|
||||||
|
).splitlines()[:10]
|
||||||
|
content_tree = tree.add("[dim]Preview (first 10 lines):[/]")
|
||||||
|
for line in preview_lines:
|
||||||
|
content_tree.add(f"[dim]{line}[/]")
|
||||||
|
elif self.file_type in {FileType.JSON, FileType.YAML, FileType.TOML}:
|
||||||
|
raw = self.load_file()
|
||||||
|
if raw is not None:
|
||||||
|
preview_str = (
|
||||||
|
json.dumps(raw, indent=2)
|
||||||
|
if isinstance(raw, dict)
|
||||||
|
else str(raw)
|
||||||
|
)
|
||||||
|
preview_lines = preview_str.splitlines()[:10]
|
||||||
|
content_tree = tree.add("[dim]Parsed preview:[/]")
|
||||||
|
for line in preview_lines:
|
||||||
|
content_tree.add(f"[dim]{line}[/]")
|
||||||
|
except Exception as e:
|
||||||
|
tree.add(f"[{OneColors.DARK_RED_b}]❌ Error reading file:[/] {e}")
|
||||||
|
|
||||||
|
if not parent:
|
||||||
|
self.console.print(tree)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"LoadFileAction(file_path={self.file_path}, file_type={self.file_type})"
|
@ -7,12 +7,13 @@ import random
|
|||||||
from concurrent.futures import ProcessPoolExecutor
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Sequence
|
||||||
|
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
from falyx.context import ExecutionContext, SharedContext
|
from falyx.context import ExecutionContext, SharedContext
|
||||||
|
from falyx.exceptions import EmptyPoolError
|
||||||
from falyx.execution_registry import ExecutionRegistry as er
|
from falyx.execution_registry import ExecutionRegistry as er
|
||||||
from falyx.hook_manager import HookManager, HookType
|
from falyx.hook_manager import HookManager, HookType
|
||||||
from falyx.logger import logger
|
from falyx.logger import logger
|
||||||
@ -37,7 +38,7 @@ class ProcessPoolAction(BaseAction):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
actions: list[ProcessTask] | None = None,
|
actions: Sequence[ProcessTask] | None = None,
|
||||||
*,
|
*,
|
||||||
hooks: HookManager | None = None,
|
hooks: HookManager | None = None,
|
||||||
executor: ProcessPoolExecutor | None = None,
|
executor: ProcessPoolExecutor | None = None,
|
||||||
@ -56,7 +57,7 @@ class ProcessPoolAction(BaseAction):
|
|||||||
if actions:
|
if actions:
|
||||||
self.set_actions(actions)
|
self.set_actions(actions)
|
||||||
|
|
||||||
def set_actions(self, actions: list[ProcessTask]) -> None:
|
def set_actions(self, actions: Sequence[ProcessTask]) -> None:
|
||||||
"""Replaces the current action list with a new one."""
|
"""Replaces the current action list with a new one."""
|
||||||
self.actions.clear()
|
self.actions.clear()
|
||||||
for action in actions:
|
for action in actions:
|
||||||
@ -78,6 +79,8 @@ class ProcessPoolAction(BaseAction):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
async def _run(self, *args, **kwargs) -> Any:
|
async def _run(self, *args, **kwargs) -> Any:
|
||||||
|
if not self.actions:
|
||||||
|
raise EmptyPoolError(f"[{self.name}] No actions to execute.")
|
||||||
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
|
||||||
if self.shared_context:
|
if self.shared_context:
|
||||||
shared_context.set_shared_result(self.shared_context.last_result())
|
shared_context.set_shared_result(self.shared_context.last_result())
|
||||||
|
@ -107,7 +107,10 @@ class SelectFileAction(BaseAction):
|
|||||||
def _coerce_return_type(self, return_type: FileType | str) -> FileType:
|
def _coerce_return_type(self, return_type: FileType | str) -> FileType:
|
||||||
if isinstance(return_type, FileType):
|
if isinstance(return_type, FileType):
|
||||||
return return_type
|
return return_type
|
||||||
|
elif isinstance(return_type, str):
|
||||||
return FileType(return_type)
|
return FileType(return_type)
|
||||||
|
else:
|
||||||
|
raise TypeError("return_type must be a FileType enum or string")
|
||||||
|
|
||||||
def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
|
def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
|
||||||
value: Any
|
value: Any
|
||||||
|
@ -19,7 +19,7 @@ in building robust interactive menus.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
from typing import Any, Callable
|
from typing import Any, Awaitable, Callable
|
||||||
|
|
||||||
from prompt_toolkit.formatted_text import FormattedText
|
from prompt_toolkit.formatted_text import FormattedText
|
||||||
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
||||||
@ -105,7 +105,7 @@ class Command(BaseModel):
|
|||||||
|
|
||||||
key: str
|
key: str
|
||||||
description: str
|
description: str
|
||||||
action: BaseAction | Callable[..., Any]
|
action: BaseAction | Callable[..., Any] | Callable[..., Awaitable[Any]]
|
||||||
args: tuple = ()
|
args: tuple = ()
|
||||||
kwargs: dict[str, Any] = Field(default_factory=dict)
|
kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||||
hidden: bool = False
|
hidden: bool = False
|
||||||
|
@ -30,5 +30,13 @@ class EmptyChainError(FalyxError):
|
|||||||
"""Exception raised when the chain is empty."""
|
"""Exception raised when the chain is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyGroupError(FalyxError):
|
||||||
|
"""Exception raised when the chain is empty."""
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyPoolError(FalyxError):
|
||||||
|
"""Exception raised when the chain is empty."""
|
||||||
|
|
||||||
|
|
||||||
class CommandArgumentError(FalyxError):
|
class CommandArgumentError(FalyxError):
|
||||||
"""Exception raised when there is an error in the command argument parser."""
|
"""Exception raised when there is an error in the command argument parser."""
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
"""logger.py"""
|
"""logger.py"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("falyx")
|
logger: logging.Logger = logging.getLogger("falyx")
|
||||||
|
@ -46,6 +46,7 @@ class Argument:
|
|||||||
ArgumentAction.STORE,
|
ArgumentAction.STORE,
|
||||||
ArgumentAction.APPEND,
|
ArgumentAction.APPEND,
|
||||||
ArgumentAction.EXTEND,
|
ArgumentAction.EXTEND,
|
||||||
|
ArgumentAction.ACTION,
|
||||||
)
|
)
|
||||||
and not self.positional
|
and not self.positional
|
||||||
):
|
):
|
||||||
@ -54,6 +55,7 @@ class Argument:
|
|||||||
ArgumentAction.STORE,
|
ArgumentAction.STORE,
|
||||||
ArgumentAction.APPEND,
|
ArgumentAction.APPEND,
|
||||||
ArgumentAction.EXTEND,
|
ArgumentAction.EXTEND,
|
||||||
|
ArgumentAction.ACTION,
|
||||||
) or isinstance(self.nargs, str):
|
) or isinstance(self.nargs, str):
|
||||||
choice_text = self.dest
|
choice_text = self.dest
|
||||||
|
|
||||||
|
@ -177,7 +177,6 @@ class CommandArgumentParser:
|
|||||||
else:
|
else:
|
||||||
choices = []
|
choices = []
|
||||||
for choice in choices:
|
for choice in choices:
|
||||||
if not isinstance(choice, expected_type):
|
|
||||||
try:
|
try:
|
||||||
coerce_value(choice, expected_type)
|
coerce_value(choice, expected_type)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@ -190,7 +189,7 @@ class CommandArgumentParser:
|
|||||||
self, default: Any, expected_type: type, dest: str
|
self, default: Any, expected_type: type, dest: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate the default value type."""
|
"""Validate the default value type."""
|
||||||
if default is not None and not isinstance(default, expected_type):
|
if default is not None:
|
||||||
try:
|
try:
|
||||||
coerce_value(default, expected_type)
|
coerce_value(default, expected_type)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@ -203,7 +202,6 @@ class CommandArgumentParser:
|
|||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(default, list):
|
if isinstance(default, list):
|
||||||
for item in default:
|
for item in default:
|
||||||
if not isinstance(item, expected_type):
|
|
||||||
try:
|
try:
|
||||||
coerce_value(item, expected_type)
|
coerce_value(item, expected_type)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@ -422,22 +420,22 @@ class CommandArgumentParser:
|
|||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Expected at least one value for '{spec.dest}'"
|
f"Expected at least one value for '{spec.dest}'"
|
||||||
)
|
)
|
||||||
while i < len(args) and not args[i].startswith("-"):
|
while i < len(args) and args[i] not in self._keyword:
|
||||||
values.append(args[i])
|
values.append(args[i])
|
||||||
i += 1
|
i += 1
|
||||||
assert values, "Expected at least one value for '+' nargs: shouldn't happen"
|
assert values, "Expected at least one value for '+' nargs: shouldn't happen"
|
||||||
return values, i
|
return values, i
|
||||||
elif spec.nargs == "*":
|
elif spec.nargs == "*":
|
||||||
while i < len(args) and not args[i].startswith("-"):
|
while i < len(args) and args[i] not in self._keyword:
|
||||||
values.append(args[i])
|
values.append(args[i])
|
||||||
i += 1
|
i += 1
|
||||||
return values, i
|
return values, i
|
||||||
elif spec.nargs == "?":
|
elif spec.nargs == "?":
|
||||||
if i < len(args) and not args[i].startswith("-"):
|
if i < len(args) and args[i] not in self._keyword:
|
||||||
return [args[i]], i + 1
|
return [args[i]], i + 1
|
||||||
return [], i
|
return [], i
|
||||||
elif spec.nargs is None:
|
elif spec.nargs is None:
|
||||||
if i < len(args) and not args[i].startswith("-"):
|
if i < len(args) and args[i] not in self._keyword:
|
||||||
return [args[i]], i + 1
|
return [args[i]], i + 1
|
||||||
return [], i
|
return [], i
|
||||||
assert False, "Invalid nargs value: shouldn't happen"
|
assert False, "Invalid nargs value: shouldn't happen"
|
||||||
@ -524,10 +522,9 @@ class CommandArgumentParser:
|
|||||||
|
|
||||||
return i
|
return i
|
||||||
|
|
||||||
def _expand_posix_bundling(self, args: list[str]) -> list[str]:
|
def _expand_posix_bundling(self, token: str) -> list[str] | str:
|
||||||
"""Expand POSIX-style bundled arguments into separate arguments."""
|
"""Expand POSIX-style bundled arguments into separate arguments."""
|
||||||
expanded = []
|
expanded = []
|
||||||
for token in args:
|
|
||||||
if token.startswith("-") and not token.startswith("--") and len(token) > 2:
|
if token.startswith("-") and not token.startswith("--") and len(token) > 2:
|
||||||
# POSIX bundle
|
# POSIX bundle
|
||||||
# e.g. -abc -> -a -b -c
|
# e.g. -abc -> -a -b -c
|
||||||
@ -538,26 +535,20 @@ class CommandArgumentParser:
|
|||||||
raise CommandArgumentError(f"Unrecognized option: {flag}")
|
raise CommandArgumentError(f"Unrecognized option: {flag}")
|
||||||
expanded.append(flag)
|
expanded.append(flag)
|
||||||
else:
|
else:
|
||||||
expanded.append(token)
|
return token
|
||||||
return expanded
|
return expanded
|
||||||
|
|
||||||
async def parse_args(
|
async def _handle_token(
|
||||||
self, args: list[str] | None = None, from_validate: bool = False
|
self,
|
||||||
) -> dict[str, Any]:
|
token: str,
|
||||||
"""Parse Falyx Command arguments."""
|
args: list[str],
|
||||||
if args is None:
|
i: int,
|
||||||
args = []
|
result: dict[str, Any],
|
||||||
|
positional_args: list[Argument],
|
||||||
args = self._expand_posix_bundling(args)
|
consumed_positional_indices: set[int],
|
||||||
|
consumed_indices: set[int],
|
||||||
result = {arg.dest: deepcopy(arg.default) for arg in self._arguments}
|
from_validate: bool = False,
|
||||||
positional_args = [arg for arg in self._arguments if arg.positional]
|
) -> int:
|
||||||
consumed_positional_indices: set[int] = set()
|
|
||||||
consumed_indices: set[int] = set()
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < len(args):
|
|
||||||
token = args[i]
|
|
||||||
if token in self._keyword:
|
if token in self._keyword:
|
||||||
spec = self._keyword[token]
|
spec = self._keyword[token]
|
||||||
action = spec.action
|
action = spec.action
|
||||||
@ -572,9 +563,7 @@ class CommandArgumentParser:
|
|||||||
), "resolver should be an instance of BaseAction"
|
), "resolver should be an instance of BaseAction"
|
||||||
values, new_i = self._consume_nargs(args, i + 1, spec)
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
||||||
try:
|
try:
|
||||||
typed_values = [
|
typed_values = [coerce_value(value, spec.type) for value in values]
|
||||||
coerce_value(value, spec.type) for value in values
|
|
||||||
]
|
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Invalid value for '{spec.dest}': {error}"
|
f"Invalid value for '{spec.dest}': {error}"
|
||||||
@ -603,9 +592,7 @@ class CommandArgumentParser:
|
|||||||
assert result.get(spec.dest) is not None, "dest should not be None"
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
||||||
values, new_i = self._consume_nargs(args, i + 1, spec)
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
||||||
try:
|
try:
|
||||||
typed_values = [
|
typed_values = [coerce_value(value, spec.type) for value in values]
|
||||||
coerce_value(value, spec.type) for value in values
|
|
||||||
]
|
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Invalid value for '{spec.dest}': {error}"
|
f"Invalid value for '{spec.dest}': {error}"
|
||||||
@ -620,9 +607,7 @@ class CommandArgumentParser:
|
|||||||
assert result.get(spec.dest) is not None, "dest should not be None"
|
assert result.get(spec.dest) is not None, "dest should not be None"
|
||||||
values, new_i = self._consume_nargs(args, i + 1, spec)
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
||||||
try:
|
try:
|
||||||
typed_values = [
|
typed_values = [coerce_value(value, spec.type) for value in values]
|
||||||
coerce_value(value, spec.type) for value in values
|
|
||||||
]
|
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Invalid value for '{spec.dest}': {error}"
|
f"Invalid value for '{spec.dest}': {error}"
|
||||||
@ -633,9 +618,7 @@ class CommandArgumentParser:
|
|||||||
else:
|
else:
|
||||||
values, new_i = self._consume_nargs(args, i + 1, spec)
|
values, new_i = self._consume_nargs(args, i + 1, spec)
|
||||||
try:
|
try:
|
||||||
typed_values = [
|
typed_values = [coerce_value(value, spec.type) for value in values]
|
||||||
coerce_value(value, spec.type) for value in values
|
|
||||||
]
|
|
||||||
except ValueError as error:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Invalid value for '{spec.dest}': {error}"
|
f"Invalid value for '{spec.dest}': {error}"
|
||||||
@ -644,10 +627,7 @@ class CommandArgumentParser:
|
|||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Expected at least one value for '{spec.dest}'"
|
f"Expected at least one value for '{spec.dest}'"
|
||||||
)
|
)
|
||||||
if (
|
if spec.nargs in (None, 1, "?") and spec.action != ArgumentAction.APPEND:
|
||||||
spec.nargs in (None, 1, "?")
|
|
||||||
and spec.action != ArgumentAction.APPEND
|
|
||||||
):
|
|
||||||
result[spec.dest] = (
|
result[spec.dest] = (
|
||||||
typed_values[0] if len(typed_values) == 1 else typed_values
|
typed_values[0] if len(typed_values) == 1 else typed_values
|
||||||
)
|
)
|
||||||
@ -662,9 +642,11 @@ class CommandArgumentParser:
|
|||||||
# Get the next flagged argument index if it exists
|
# Get the next flagged argument index if it exists
|
||||||
next_flagged_index = -1
|
next_flagged_index = -1
|
||||||
for index, arg in enumerate(args[i:], start=i):
|
for index, arg in enumerate(args[i:], start=i):
|
||||||
if arg.startswith("-"):
|
if arg in self._keyword:
|
||||||
next_flagged_index = index
|
next_flagged_index = index
|
||||||
break
|
break
|
||||||
|
print(f"next_flagged_index: {next_flagged_index}")
|
||||||
|
print(f"{self._keyword_list=}")
|
||||||
if next_flagged_index == -1:
|
if next_flagged_index == -1:
|
||||||
next_flagged_index = len(args)
|
next_flagged_index = len(args)
|
||||||
args_consumed = await self._consume_all_positional_args(
|
args_consumed = await self._consume_all_positional_args(
|
||||||
@ -674,6 +656,38 @@ class CommandArgumentParser:
|
|||||||
consumed_positional_indices,
|
consumed_positional_indices,
|
||||||
)
|
)
|
||||||
i += args_consumed
|
i += args_consumed
|
||||||
|
return i
|
||||||
|
|
||||||
|
async def parse_args(
|
||||||
|
self, args: list[str] | None = None, from_validate: bool = False
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Parse Falyx Command arguments."""
|
||||||
|
if args is None:
|
||||||
|
args = []
|
||||||
|
|
||||||
|
result = {arg.dest: deepcopy(arg.default) for arg in self._arguments}
|
||||||
|
positional_args: list[Argument] = [
|
||||||
|
arg for arg in self._arguments if arg.positional
|
||||||
|
]
|
||||||
|
consumed_positional_indices: set[int] = set()
|
||||||
|
consumed_indices: set[int] = set()
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
token = self._expand_posix_bundling(args[i])
|
||||||
|
if isinstance(token, list):
|
||||||
|
args[i : i + 1] = token
|
||||||
|
token = args[i]
|
||||||
|
i = await self._handle_token(
|
||||||
|
token,
|
||||||
|
args,
|
||||||
|
i,
|
||||||
|
result,
|
||||||
|
positional_args,
|
||||||
|
consumed_positional_indices,
|
||||||
|
consumed_indices,
|
||||||
|
from_validate=from_validate,
|
||||||
|
)
|
||||||
|
|
||||||
# Required validation
|
# Required validation
|
||||||
for spec in self._arguments:
|
for spec in self._arguments:
|
||||||
@ -797,6 +811,8 @@ class CommandArgumentParser:
|
|||||||
flags = arg.get_positional_text()
|
flags = arg.get_positional_text()
|
||||||
arg_line = Text(f" {flags:<30} ")
|
arg_line = Text(f" {flags:<30} ")
|
||||||
help_text = arg.help or ""
|
help_text = arg.help or ""
|
||||||
|
if help_text and len(flags) > 30:
|
||||||
|
help_text = f"\n{'':<33}{help_text}"
|
||||||
arg_line.append(help_text)
|
arg_line.append(help_text)
|
||||||
self.console.print(arg_line)
|
self.console.print(arg_line)
|
||||||
self.console.print("[bold]options:[/bold]")
|
self.console.print("[bold]options:[/bold]")
|
||||||
@ -805,6 +821,8 @@ class CommandArgumentParser:
|
|||||||
flags_choice = f"{flags} {arg.get_choice_text()}"
|
flags_choice = f"{flags} {arg.get_choice_text()}"
|
||||||
arg_line = Text(f" {flags_choice:<30} ")
|
arg_line = Text(f" {flags_choice:<30} ")
|
||||||
help_text = arg.help or ""
|
help_text = arg.help or ""
|
||||||
|
if help_text and len(flags_choice) > 30:
|
||||||
|
help_text = f"\n{'':<33}{help_text}"
|
||||||
arg_line.append(help_text)
|
arg_line.append(help_text)
|
||||||
self.console.print(arg_line)
|
self.console.print(arg_line)
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ def coerce_enum(value: Any, enum_type: EnumMeta) -> Any:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
base_type = type(next(iter(enum_type)).value)
|
base_type = type(next(iter(enum_type)).value)
|
||||||
print(base_type)
|
|
||||||
try:
|
try:
|
||||||
coerced_value = base_type(value)
|
coerced_value = base_type(value)
|
||||||
return enum_type(coerced_value)
|
return enum_type(coerced_value)
|
||||||
|
@ -2,14 +2,16 @@
|
|||||||
"""protocols.py"""
|
"""protocols.py"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Awaitable, Protocol, runtime_checkable
|
from typing import Any, Awaitable, Callable, Protocol, runtime_checkable
|
||||||
|
|
||||||
from falyx.action.base_action import BaseAction
|
from falyx.action.base_action import BaseAction
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class ActionFactoryProtocol(Protocol):
|
class ActionFactoryProtocol(Protocol):
|
||||||
async def __call__(self, *args: Any, **kwargs: Any) -> Awaitable[BaseAction]: ...
|
async def __call__(
|
||||||
|
self, *args: Any, **kwargs: Any
|
||||||
|
) -> Callable[..., Awaitable[BaseAction]]: ...
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
|
@ -1 +1 @@
|
|||||||
__version__ = "0.1.53"
|
__version__ = "0.1.54"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.53"
|
version = "0.1.54"
|
||||||
description = "Reliable and introspectable async CLI action framework."
|
description = "Reliable and introspectable async CLI action framework."
|
||||||
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -27,6 +27,10 @@ black = { version = "^25.0", allow-prereleases = true }
|
|||||||
mypy = { version = "^1.0", allow-prereleases = true }
|
mypy = { version = "^1.0", allow-prereleases = true }
|
||||||
isort = { version = "^5.0", allow-prereleases = true }
|
isort = { version = "^5.0", allow-prereleases = true }
|
||||||
pytest-cov = "^4.0"
|
pytest-cov = "^4.0"
|
||||||
|
mkdocs = "^1.6.1"
|
||||||
|
mkdocs-material = "^9.6.14"
|
||||||
|
mkdocstrings = {extras = ["python"], version = "^0.29.1"}
|
||||||
|
mike = "^2.1.3"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
falyx = "falyx.__main__:main"
|
falyx = "falyx.__main__:main"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from falyx.action import Action, ActionFactoryAction, ChainedAction
|
from falyx.action import Action, ActionFactory, ChainedAction
|
||||||
|
|
||||||
|
|
||||||
def make_chain(value) -> ChainedAction:
|
def make_chain(value) -> ChainedAction:
|
||||||
@ -16,9 +16,7 @@ def make_chain(value) -> ChainedAction:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_action_factory_action():
|
async def test_action_factory_action():
|
||||||
action = ActionFactoryAction(
|
action = ActionFactory(name="test_action", factory=make_chain, args=("test_value",))
|
||||||
name="test_action", factory=make_chain, args=("test_value",)
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await action()
|
result = await action()
|
||||||
|
|
||||||
|
26
tests/test_parsers/test_negative_numbers.py
Normal file
26
tests/test_parsers/test_negative_numbers.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from falyx.exceptions import CommandArgumentError
|
||||||
|
from falyx.parser import CommandArgumentParser
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_negative_integer():
|
||||||
|
parser = CommandArgumentParser()
|
||||||
|
parser.add_argument("--number", type=int, required=True, help="A negative integer")
|
||||||
|
args = await parser.parse_args(["--number", "-42"])
|
||||||
|
assert args["number"] == -42
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_negative_float():
|
||||||
|
parser = CommandArgumentParser()
|
||||||
|
parser.add_argument("--value", type=float, required=True, help="A negative float")
|
||||||
|
args = await parser.parse_args(["--value", "-3.14"])
|
||||||
|
assert args["value"] == -3.14
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_number_flag():
|
||||||
|
parser = CommandArgumentParser()
|
||||||
|
with pytest.raises(CommandArgumentError):
|
||||||
|
parser.add_argument("-1", type=int, required=True, help="A negative number flag")
|
Reference in New Issue
Block a user