Bubble up errors from CAP, catch a broader exception when parsing arguments, add type parsing to arg_metadata
This commit is contained in:
parent
e3ebc1b17b
commit
09eeb90dc6
|
@ -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())
|
|
@ -746,12 +746,10 @@ class Falyx:
|
||||||
"""
|
"""
|
||||||
table = Table(title=self.title, show_header=False, box=box.SIMPLE) # type: ignore[arg-type]
|
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]
|
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):
|
for chunk in chunks(visible_commands, self.columns):
|
||||||
row = []
|
row = []
|
||||||
for key, command in chunk:
|
for key, command in chunk:
|
||||||
cell = f"[{key}] [{command.style}]{command.description}"
|
row.append(f"[{key}] [{command.style}]{command.description}")
|
||||||
row.append(f"{cell:<{space}}")
|
|
||||||
table.add_row(*row)
|
table.add_row(*row)
|
||||||
bottom_row = self.get_bottom_row()
|
bottom_row = self.get_bottom_row()
|
||||||
for row in chunks(bottom_row, self.columns):
|
for row in chunks(bottom_row, self.columns):
|
||||||
|
@ -811,7 +809,7 @@ class Falyx:
|
||||||
args, kwargs = await name_map[choice].parse_args(
|
args, kwargs = await name_map[choice].parse_args(
|
||||||
input_args, from_validate
|
input_args, from_validate
|
||||||
)
|
)
|
||||||
except CommandArgumentError as error:
|
except (CommandArgumentError, Exception) as error:
|
||||||
if not from_validate:
|
if not from_validate:
|
||||||
name_map[choice].show_help()
|
name_map[choice].show_help()
|
||||||
self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}")
|
self.console.print(f"[{OneColors.DARK_RED}]❌ [{choice}]: {error}")
|
||||||
|
|
|
@ -292,10 +292,10 @@ class CommandArgumentParser:
|
||||||
if not isinstance(choice, expected_type):
|
if not isinstance(choice, expected_type):
|
||||||
try:
|
try:
|
||||||
coerce_value(choice, expected_type)
|
coerce_value(choice, expected_type)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
raise CommandArgumentError(
|
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
|
return choices
|
||||||
|
|
||||||
def _validate_default_type(
|
def _validate_default_type(
|
||||||
|
@ -305,10 +305,10 @@ class CommandArgumentParser:
|
||||||
if default is not None and not isinstance(default, expected_type):
|
if default is not None and not isinstance(default, expected_type):
|
||||||
try:
|
try:
|
||||||
coerce_value(default, expected_type)
|
coerce_value(default, expected_type)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
raise CommandArgumentError(
|
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(
|
def _validate_default_list_type(
|
||||||
self, default: list[Any], expected_type: type, dest: str
|
self, default: list[Any], expected_type: type, dest: str
|
||||||
|
@ -318,10 +318,10 @@ class CommandArgumentParser:
|
||||||
if not isinstance(item, expected_type):
|
if not isinstance(item, expected_type):
|
||||||
try:
|
try:
|
||||||
coerce_value(item, expected_type)
|
coerce_value(item, expected_type)
|
||||||
except Exception:
|
except Exception as error:
|
||||||
raise CommandArgumentError(
|
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(
|
def _validate_resolver(
|
||||||
self, action: ArgumentAction, resolver: BaseAction | None
|
self, action: ArgumentAction, resolver: BaseAction | None
|
||||||
|
@ -597,10 +597,10 @@ class CommandArgumentParser:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
typed = [coerce_value(value, spec.type) for value in values]
|
typed = [coerce_value(value, spec.type) for value in values]
|
||||||
except Exception:
|
except Exception as error:
|
||||||
raise CommandArgumentError(
|
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:
|
if spec.action == ArgumentAction.ACTION:
|
||||||
assert isinstance(
|
assert isinstance(
|
||||||
spec.resolver, BaseAction
|
spec.resolver, BaseAction
|
||||||
|
@ -684,10 +684,10 @@ class CommandArgumentParser:
|
||||||
typed_values = [
|
typed_values = [
|
||||||
coerce_value(value, spec.type) for value in values
|
coerce_value(value, spec.type) for value in values
|
||||||
]
|
]
|
||||||
except ValueError:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Invalid value for '{spec.dest}': expected {spec.type.__name__}"
|
f"Invalid value for '{spec.dest}': {error}"
|
||||||
)
|
) from error
|
||||||
try:
|
try:
|
||||||
result[spec.dest] = await spec.resolver(*typed_values)
|
result[spec.dest] = await spec.resolver(*typed_values)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
|
@ -715,10 +715,10 @@ class CommandArgumentParser:
|
||||||
typed_values = [
|
typed_values = [
|
||||||
coerce_value(value, spec.type) for value in values
|
coerce_value(value, spec.type) for value in values
|
||||||
]
|
]
|
||||||
except ValueError:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
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:
|
if spec.nargs is None:
|
||||||
result[spec.dest].append(spec.type(values[0]))
|
result[spec.dest].append(spec.type(values[0]))
|
||||||
else:
|
else:
|
||||||
|
@ -732,10 +732,10 @@ class CommandArgumentParser:
|
||||||
typed_values = [
|
typed_values = [
|
||||||
coerce_value(value, spec.type) for value in values
|
coerce_value(value, spec.type) for value in values
|
||||||
]
|
]
|
||||||
except ValueError:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
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)
|
result[spec.dest].extend(typed_values)
|
||||||
consumed_indices.update(range(i, new_i))
|
consumed_indices.update(range(i, new_i))
|
||||||
i = new_i
|
i = new_i
|
||||||
|
@ -745,10 +745,10 @@ class CommandArgumentParser:
|
||||||
typed_values = [
|
typed_values = [
|
||||||
coerce_value(value, spec.type) for value in values
|
coerce_value(value, spec.type) for value in values
|
||||||
]
|
]
|
||||||
except ValueError:
|
except ValueError as error:
|
||||||
raise CommandArgumentError(
|
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 ("*", "?"):
|
if not typed_values and spec.nargs not in ("*", "?"):
|
||||||
raise CommandArgumentError(
|
raise CommandArgumentError(
|
||||||
f"Expected at least one value for '{spec.dest}'"
|
f"Expected at least one value for '{spec.dest}'"
|
||||||
|
|
|
@ -31,8 +31,13 @@ def infer_args_from_func(
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if metadata.get("type"):
|
||||||
|
arg_type = metadata["type"]
|
||||||
|
else:
|
||||||
arg_type = (
|
arg_type = (
|
||||||
param.annotation if param.annotation is not inspect.Parameter.empty else str
|
param.annotation
|
||||||
|
if param.annotation is not inspect.Parameter.empty
|
||||||
|
else str
|
||||||
)
|
)
|
||||||
if isinstance(arg_type, str):
|
if isinstance(arg_type, str):
|
||||||
arg_type = str
|
arg_type = str
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = "0.1.47"
|
__version__ = "0.1.48"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "falyx"
|
name = "falyx"
|
||||||
version = "0.1.47"
|
version = "0.1.48"
|
||||||
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"
|
||||||
|
|
Loading…
Reference in New Issue