Is there an idiomatic way, using the Python Click library, to create a command where one option depends on a value set by a previous option?
A concrete example (my use case) would be that a command takes an option of type click.File
as input, but also an encoding option which specifies the encoding of the input stream:
import click
@click.command()
@click.option("--encoding", type=str, default="utf-8")
@click.option("--input",
type=click.File("r", encoding="CAN I SET THIS DYNAMICALLY BASED ON --encoding?"))
def cli(encoding, input):
pass
I guess it would have to involve some kind of deferred evaluation using a callable, but I'm not sure if it's even possible given the current Click API.
I've figured out I can do something along the following lines:
import click
@click.command()
@click.pass_context
@click.option("--encoding", type=str, default="utf-8")
@click.option("--input", type=str, default="-")
def cli(ctx, encoding, input):
input = click.File("r", encoding=encoding)(input, ctx=ctx)
But it somehow feels less readable / maintainable to decouple the option decorator from the semantically correct type constraint that applies to it, and put str
in there instead as a dummy. So if there's a way to keep these two together, please enlighten me.
A proposed workaround:
I guess I could use the click.File
type twice, making it lazy in the decorator so that the file isn't actually left opened, the first time around:
@click.option("--input", type=click.File("r", lazy=True), default="-")
This feels semantically more satisfying, but also redundant.
It is possible to inherit from the click.File
class and override the .convert()
method to allow it to gather the encoding value from the context.
It should look something like:
@click.command()
@click.option("--my_encoding", type=str, default="utf-8")
@click.option("--in_file", type=CustomFile("r", encoding_option_name="my_encoding"))
def cli(my_encoding, in_file):
....
CustomFile
should allow the user to specify whichever name they want for the parameter from which the encoding value should be collected, but there can be a reasonable default such as "encoding".
This CustomFile class can be used in association with an encoding option:
import click
class CustomFile(click.File):
"""
A custom `click.File` class which will set its encoding to
a parameter.
:param encoding_option_name: The 'name' of the encoding parameter
"""
def __init__(self, *args, encoding_option_name="encoding", **kwargs):
# enforce a lazy file, so that opening the file is deferred until after
# all of the command line parameters have been processed (--encoding
# might be specified after --in_file)
kwargs['lazy'] = True
# Python 3 can use just super()
super(CustomFile, self).__init__(*args, **kwargs)
self.lazy_file = None
self.encoding_option_name = encoding_option_name
def convert(self, value, param, ctx):
"""During convert, get the encoding from the context."""
if self.encoding_option_name not in ctx.params:
# if the encoding option has not been processed yet, wrap its
# convert hook so that it also retroactively modifies the encoding
# attribute on self and self.lazy_file
encoding_opt = [
c for c in ctx.command.params
if self.encoding_option_name == c.human_readable_name]
assert encoding_opt, \
"option '{}' not found for encoded_file".format(
self.encoding_option_name)
encoding_type = encoding_opt[0].type
encoding_convert = encoding_type.convert
def encoding_convert_hook(*convert_args):
encoding_type.convert = encoding_convert
self.encoding = encoding_type.convert(*convert_args)
self.lazy_file.encoding = self.encoding
return self.encoding
encoding_type.convert = encoding_convert_hook
else:
# if it has already been processed, just use the value
self.encoding = ctx.params[self.encoding_option_name]
# Python 3 can use just super()
self.lazy_file = super(CustomFile, self).convert(value, param, ctx)
return self.lazy_file
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With