1
0
mirror of https://github.com/jbranchaud/til synced 2026-07-02 15:49:44 +00:00

Add Check Precondition Before Click Arg Parsing as a Python TIL

This commit is contained in:
jbranchaud
2026-05-24 17:35:11 -05:00
parent 539cbbefa6
commit cea6c75e1c
2 changed files with 55 additions and 1 deletions
+2 -1
View File
@@ -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).
_1796 TILs and counting..._
_1797 TILs and counting..._
See some of the other learning resources I work on:
@@ -1061,6 +1061,7 @@ If you've learned something here, support my efforts writing daily TILs by
- [Avoid Modification With Frozen Dataclass](python/avoid-modification-with-frozen-dataclass.md)
- [Break Debugger On First Line Of Program](python/break-debugger-on-first-line-of-program.md)
- [Check If Package Is Installed With Pip](python/check-if-package-is-installed-with-pip.md)
- [Check Precondition Before Click Arg Parsing](python/check-precondition-before-click-arg-parsing.md)
- [Control Passing Of Time In Tests](python/control-passing-of-time-in-tests.md)
- [Create A Dummy DataFrame In Pandas](python/create-a-dummy-dataframe-in-pandas.md)
- [Create A Range Of Descending Values](python/create-a-range-of-descending-values.md)
@@ -0,0 +1,53 @@
# Check Precondition Before Click Arg Parsing
When setting up various [Click](https://click.palletsprojects.com/en/stable/)
subcommands with options, I ran into an issue with the order of some validation
checks. I was putting the same precondition validation logic at the beginning of
several subcommands. I was also putting callback validations on specific options
to those subcommands. Ideally the option validations could rely on those
precondition validations. However, the option callbacks run before anything in
the body of the subcommands.
The solution was to move those preconditions out of the subcommand body
(simplifying the subcommand) and into a `click.Command` subclass.
To demonstrate that, I'll first show the `click.Command` subclass:
```python
class RequireActiveSessionCommand(click.Command):
def parse_args(self, ctx, args):
if ctx.obj.active_session is None:
msg = "No active session being tracked. Start a session first."
raise click.UsageError(msg)
return super().parse_args(ctx, args)
```
The only thing this subclass overrides is `parse_args` where it gets ahead of
the standard arg parsing logic to first check the precondition. In this case, I
check that there is an active session. If there isn't, then I can raise a
`click.UsageError`. Otherwise, it delegates back to the super-class
implementation of `parse_args`.
This subclass then gets used for the commands that need to enforce this
precondition. Two prime examples of that are the `stop` and `cancel` subcommands.
```python
@cli.command(cls=RequireActiveSessionCommand)
@click.option("--at", help='Hours previous to end the timer, e.g. "2 hours ago"', callback=validate_stop_at)
@pass_cli
def stop(cli_ctx: CliContext, at: datetime) -> None:
# ... implementation omitted
@cli.command(cls=RequireActiveSessionCommand)
@pass_cli
def cancel(cli_ctx: CliContext):
# ... implementation omitted
```
Other subcommands, like `start` and `status` that don't need to enforce this
precondition use the `@cli.command()` decorator without passing in a custom
subclass.
This example is pulled directly from [this commit](https://github.com/jbranchaud/py-vmt/commit/505109b7a4013e05f085cded666c6b1ac7c3c250)
of my [`py-vmt` time tracker tool](https://github.com/jbranchaud/py-vmt).