From e58ffffda08a0287415b1a76df5e436b74399233 Mon Sep 17 00:00:00 2001 From: jbranchaud Date: Sat, 9 May 2026 20:28:01 -0500 Subject: [PATCH] Add Validate Click Option With Callback as a Python TIL --- README.md | 3 +- python/validate-click-option-with-callback.md | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 python/validate-click-option-with-callback.md diff --git a/README.md b/README.md index 78e6679..02da8d3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ working across different projects via [VisualMode](https://www.visualmode.dev/). For a steady stream of TILs, [sign up for my newsletter](https://visualmode.kit.com/newsletter). -_1790 TILs and counting..._ +_1791 TILs and counting..._ See some of the other learning resources I work on: @@ -1085,6 +1085,7 @@ If you've learned something here, support my efforts writing daily TILs by - [Use pipx To Install End User Apps](python/use-pipx-to-install-end-user-apps.md) - [Use `__post_init__` For `dataclass` Validations](python/use-post-init-for-dataclass-validations.md) - [Use Verbose Flag To Get More Diff](python/use-verbose-flag-to-get-more-diff.md) +- [Validate Click Option With Callback](python/validate-click-option-with-callback.md) ### Rails diff --git a/python/validate-click-option-with-callback.md b/python/validate-click-option-with-callback.md new file mode 100644 index 0000000..58ca6e0 --- /dev/null +++ b/python/validate-click-option-with-callback.md @@ -0,0 +1,64 @@ +# Validate Click Option With Callback + +I have a [click](https://click.palletsprojects.com/en/stable/) subcommand in my +[`py-vmt` project](https://github.com/jbranchaud/py-vmt) that includes an +`option` specified with the `--at` flag. This is what it originally looked like: + +```python +# define `start` subcommand +@cli.command() +@click.argument("project-name") +@click.option("--at", help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"') +@pass_cli +def start(cli_ctx: CliContext, project_name: str, at: str | None) -> None: + # ... +``` + +The value of `at` needs to be in the past. I need a way validate that it is or +otherwise bail early with a useful error message. The optional +[`callback`](https://click.palletsprojects.com/en/stable/advanced/#callbacks-for-validation) +to `@click.option` plus `click.BadParameter` are a good way to handle that. + +First, I define a callback handler that does the validation. I even take it a +step further and have it return the transformed value (`datetime`) that the +subcommand logic will need. + +```python +def validate_past_time(_ctx, _param, value: str | None) -> datetime: + now = datetime.now(timezone.utc) + + if value == None: + return now + + start_at = time_helpers.parse_to_datetime(value) + + if start_time == None or start_at > now: + raise click.BadParameter("must be a relative time in the past") + + return start_at +``` + +I ignore the first two arguments because I only need to work with `value`. Value +might be something like `"33 minutes ago"` and I attempt to transform that with +`dateparser` into a `datetime` instance. If it can't be parsed or it isn't in +the past, then I raise `click.BadParameter` which presents the user with useful +usage details. + +This callback can then be incorporated into the subcommand like so: + +```python +# define `start` subcommand +@cli.command() +@click.argument("project-name") +@click.option( + "--at", + help='Relative time in past to start the time, e.g. "2 hours ago", "33 minutes ago"', + callback=validate_past_time +) +@pass_cli +def start(cli_ctx: CliContext, project_name: str, at: datetime) -> None: + # ... +``` + +Now I can expect the incoming `at` option to be a `datetime` which helps +simplify several lines of logic in the `start` implementation.