Files
falyx/falyx/parser/utils.py
Roland Thomas 7f63e16097 feat: Add module docs, Enum coercion, tracebacks, and toggle improvements
- Add comprehensive module docstrings across the codebase for better clarity and documentation.
- Refactor Enum classes (e.g., FileType, ConfirmType) to use `_missing_` for built-in coercion from strings.
- Add `encoding` attribute to `LoadFileAction`, `SaveFileAction`, and `SelectFileAction` for more flexible file handling.
- Enable lazy file loading by default in `SelectFileAction` to improve performance.
- Simplify bottom bar toggle behavior: all toggles now use `ctrl+<key>`, eliminating the need for key conflict checks with Falyx commands.
- Add `ignore_in_history` attribute to `Command` to refine how `ExecutionRegistry` identifies the last valid result.
- Improve History command output: now includes tracebacks when displaying exceptions.
2025-07-19 14:44:43 -04:00

165 lines
5.1 KiB
Python

# Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
"""
Contains value coercion and signature comparison utilities for Falyx argument parsing.
This module provides type coercion functions for converting string input into expected
Python types, including `Enum`, `bool`, `datetime`, and `Literal`. It also supports
checking whether multiple actions share identical inferred argument definitions.
Functions:
- coerce_bool: Convert a string to a boolean.
- coerce_enum: Convert a string or raw value to an Enum instance.
- coerce_value: General-purpose coercion to a target type (including nested unions, enums, etc.).
- same_argument_definitions: Check if multiple callables share the same argument structure.
"""
import types
from datetime import datetime
from enum import EnumMeta
from typing import Any, Literal, Union, get_args, get_origin
from dateutil import parser as date_parser
from falyx.action.base_action import BaseAction
from falyx.logger import logger
from falyx.parser.signature import infer_args_from_func
def coerce_bool(value: str) -> bool:
"""
Convert a string to a boolean.
Accepts various truthy and falsy representations such as 'true', 'yes', '0', 'off', etc.
Args:
value (str): The input string or boolean.
Returns:
bool: Parsed boolean result.
"""
if isinstance(value, bool):
return value
value = value.strip().lower()
if value in {"true", "t", "1", "yes", "on"}:
return True
elif value in {"false", "f", "0", "no", "off"}:
return False
return bool(value)
def coerce_enum(value: Any, enum_type: EnumMeta) -> Any:
"""
Convert a raw value or string to an Enum instance.
Tries to resolve by name, value, or coerced base type.
Args:
value (Any): The input value to convert.
enum_type (EnumMeta): The target Enum class.
Returns:
Enum: The corresponding Enum instance.
Raises:
ValueError: If the value cannot be resolved to a valid Enum member.
"""
if isinstance(value, enum_type):
return value
if isinstance(value, str):
try:
return enum_type[value]
except KeyError:
pass
base_type = type(next(iter(enum_type)).value)
try:
coerced_value = base_type(value)
return enum_type(coerced_value)
except (ValueError, TypeError):
values = [str(enum.value) for enum in enum_type]
raise ValueError(f"'{value}' should be one of {{{', '.join(values)}}}") from None
def coerce_value(value: str, target_type: type) -> Any:
"""
Attempt to convert a string to the given target type.
Handles complex typing constructs such as Union, Literal, Enum, and datetime.
Args:
value (str): The input string to convert.
target_type (type): The desired type.
Returns:
Any: The coerced value.
Raises:
ValueError: If conversion fails or the value is invalid.
"""
origin = get_origin(target_type)
args = get_args(target_type)
if origin is Literal:
if value not in args:
raise ValueError(
f"Value '{value}' is not a valid literal for type {target_type}"
)
return value
if isinstance(target_type, types.UnionType) or get_origin(target_type) is Union:
for arg in args:
try:
return coerce_value(value, arg)
except Exception:
continue
raise ValueError(f"Value '{value}' could not be coerced to any of {args}")
if isinstance(target_type, EnumMeta):
return coerce_enum(value, target_type)
if target_type is bool:
return coerce_bool(value)
if target_type is datetime:
try:
return date_parser.parse(value)
except ValueError as e:
raise ValueError(f"Value '{value}' could not be parsed as a datetime") from e
return target_type(value)
def same_argument_definitions(
actions: list[Any],
arg_metadata: dict[str, str | dict[str, Any]] | None = None,
) -> list[dict[str, Any]] | None:
"""
Determine if multiple callables resolve to the same argument definitions.
This is used to infer whether actions in an ActionGroup or ProcessPool can share
a unified argument parser.
Args:
actions (list[Any]): A list of BaseAction instances or callables.
arg_metadata (dict | None): Optional overrides for argument help or type info.
Returns:
list[dict[str, Any]] | None: The shared argument definitions if consistent, else None.
"""
arg_sets = []
for action in actions:
if isinstance(action, BaseAction):
infer_target, _ = action.get_infer_target()
arg_defs = infer_args_from_func(infer_target, arg_metadata)
elif callable(action):
arg_defs = infer_args_from_func(action, arg_metadata)
else:
logger.debug("Auto args unsupported for action: %s", action)
return None
arg_sets.append(arg_defs)
first = arg_sets[0]
if all(arg_set == first for arg_set in arg_sets[1:]):
return first
return None