Bubble up errors from CAP, catch a broader exception when parsing arguments, add type parsing to arg_metadata

This commit is contained in:
Roland Thomas Jr 2025-06-02 23:45:37 -04:00
parent e3ebc1b17b
commit 09eeb90dc6
Signed by: roland
GPG Key ID: 7C3C2B085A4C2872
6 changed files with 138 additions and 35 deletions

100
examples/type_validation.py Normal file
View File

@ -0,0 +1,100 @@
import asyncio
from uuid import UUID, uuid4
from falyx import Falyx
from falyx.parsers import CommandArgumentParser
flx = Falyx("Test Type Validation")
def uuid_val(value: str) -> str:
"""Custom validator to ensure a string is a valid UUID."""
UUID(value)
return value
async def print_uuid(uuid: str) -> str:
"""Prints the UUID if valid."""
print(f"Valid UUID: {uuid}")
return uuid
flx.add_command(
"U",
"Print a valid UUID (arguemnts)",
print_uuid,
arguments=[
{
"flags": ["uuid"],
"type": uuid_val,
"help": "A valid UUID string",
}
],
)
def uuid_parser(parser: CommandArgumentParser) -> None:
"""Custom parser to ensure the UUID argument is valid."""
parser.add_argument(
"uuid",
type=uuid_val,
help="A valid UUID string",
)
flx.add_command(
"I",
"Print a valid UUID (argument_config)",
print_uuid,
argument_config=uuid_parser,
)
flx.add_command(
"D",
"Print a valid UUID (arg_metadata)",
print_uuid,
arg_metadata={
"uuid": {
"type": uuid_val,
"help": "A valid UUID string",
}
},
)
def custom_parser(arguments: list[str]) -> tuple[tuple, dict]:
"""Custom parser to ensure the UUID argument is valid."""
if len(arguments) != 1:
raise ValueError("Exactly one argument is required")
uuid_val(arguments[0])
return (arguments[0],), {}
flx.add_command(
"C",
"Print a valid UUID (custom_parser)",
print_uuid,
custom_parser=custom_parser,
)
async def generate_uuid() -> str:
"""Generates a new UUID."""
new_uuid = uuid4()
print(f"Generated UUID: {new_uuid}")
return new_uuid
flx.add_command(
"G",
"Generate a new UUID",
lambda: print(uuid4()),
)
async def main() -> None:
await flx.run()
if __name__ == "__main__":
asyncio.run(main())

View File

@ -746,12 +746,10 @@ class Falyx:
"""
table = Table(title=self.title, show_header=False, box=box.SIMPLE) # type: ignore[arg-type]
visible_commands = [item for item in self.commands.items() if not item[1].hidden]
space = self.console.width // self.columns
for chunk in chunks(visible_commands, self.columns):
row = []
for key, command in chunk:
cell = f"[{key}] [{command.style}]{command.description}"
row.append(f"{cell:<{space}}")
row.append(f"[{key}] [{command.style}]{command.description}")
table.add_row(*row)
bottom_row = self.get_bottom_row()
for row in chunks(bottom_row, self.columns):
@ -811,7 +809,7 @@ class Falyx:
args, kwargs = await name_map[choice].parse_args(
input_args, from_validate
)
except CommandArgumentError as error:
except (CommandArgumentError, Exception) as error:
if not from_validate:
name_map[choice].show_help()
self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}")

View File

@ -292,10 +292,10 @@ class CommandArgumentParser:
if not isinstance(choice, expected_type):
try:
coerce_value(choice, expected_type)
except Exception:
except Exception as error:
raise CommandArgumentError(
f"Invalid choice {choice!r}: not coercible to {expected_type.__name__}"
)
f"Invalid choice {choice!r}: not coercible to {expected_type.__name__} error: {error}"
) from error
return choices
def _validate_default_type(
@ -305,10 +305,10 @@ class CommandArgumentParser:
if default is not None and not isinstance(default, expected_type):
try:
coerce_value(default, expected_type)
except Exception:
except Exception as error:
raise CommandArgumentError(
f"Default value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
)
f"Default value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__} error: {error}"
) from error
def _validate_default_list_type(
self, default: list[Any], expected_type: type, dest: str
@ -318,10 +318,10 @@ class CommandArgumentParser:
if not isinstance(item, expected_type):
try:
coerce_value(item, expected_type)
except Exception:
except Exception as error:
raise CommandArgumentError(
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__}"
)
f"Default list value {default!r} for '{dest}' cannot be coerced to {expected_type.__name__} error: {error}"
) from error
def _validate_resolver(
self, action: ArgumentAction, resolver: BaseAction | None
@ -597,10 +597,10 @@ class CommandArgumentParser:
try:
typed = [coerce_value(value, spec.type) for value in values]
except Exception:
except Exception as error:
raise CommandArgumentError(
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
)
f"Invalid value for '{spec.dest}': {error}"
) from error
if spec.action == ArgumentAction.ACTION:
assert isinstance(
spec.resolver, BaseAction
@ -684,10 +684,10 @@ class CommandArgumentParser:
typed_values = [
coerce_value(value, spec.type) for value in values
]
except ValueError:
except ValueError as error:
raise CommandArgumentError(
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
)
f"Invalid value for '{spec.dest}': {error}"
) from error
try:
result[spec.dest] = await spec.resolver(*typed_values)
except Exception as error:
@ -715,10 +715,10 @@ class CommandArgumentParser:
typed_values = [
coerce_value(value, spec.type) for value in values
]
except ValueError:
except ValueError as error:
raise CommandArgumentError(
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
)
f"Invalid value for '{spec.dest}': {error}"
) from error
if spec.nargs is None:
result[spec.dest].append(spec.type(values[0]))
else:
@ -732,10 +732,10 @@ class CommandArgumentParser:
typed_values = [
coerce_value(value, spec.type) for value in values
]
except ValueError:
except ValueError as error:
raise CommandArgumentError(
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
)
f"Invalid value for '{spec.dest}': {error}"
) from error
result[spec.dest].extend(typed_values)
consumed_indices.update(range(i, new_i))
i = new_i
@ -745,10 +745,10 @@ class CommandArgumentParser:
typed_values = [
coerce_value(value, spec.type) for value in values
]
except ValueError:
except ValueError as error:
raise CommandArgumentError(
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
)
f"Invalid value for '{spec.dest}': {error}"
) from error
if not typed_values and spec.nargs not in ("*", "?"):
raise CommandArgumentError(
f"Expected at least one value for '{spec.dest}'"

View File

@ -31,11 +31,16 @@ def infer_args_from_func(
):
continue
arg_type = (
param.annotation if param.annotation is not inspect.Parameter.empty else str
)
if isinstance(arg_type, str):
arg_type = str
if metadata.get("type"):
arg_type = metadata["type"]
else:
arg_type = (
param.annotation
if param.annotation is not inspect.Parameter.empty
else str
)
if isinstance(arg_type, str):
arg_type = str
default = param.default if param.default is not inspect.Parameter.empty else None
is_required = param.default is inspect.Parameter.empty
if is_required:

View File

@ -1 +1 @@
__version__ = "0.1.47"
__version__ = "0.1.48"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "falyx"
version = "0.1.47"
version = "0.1.48"
description = "Reliable and introspectable async CLI action framework."
authors = ["Roland Thomas Jr <roland@rtj.dev>"]
license = "MIT"