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]
|
||||
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}")
|
||||
|
|
|
@ -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}'"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1 +1 @@
|
|||
__version__ = "0.1.47"
|
||||
__version__ = "0.1.48"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue