-
Notifications
You must be signed in to change notification settings - Fork 189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix interactive and conditional options in click #1666
Changes from 9 commits
42281d0
5b8f241
44c5fe4
c0d7950
eeab898
03d5775
09b046a
b6acc08
de6a473
cd12859
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,30 +44,42 @@ def foo(label): | |
click.echo('Labeling with label: {}'.format(label)) | ||
""" | ||
|
||
def __init__(self, param_decls=None, switch=None, empty_ok=False, **kwargs): | ||
def __init__( | ||
self, | ||
param_decls=None, | ||
switch=None, # | ||
prompt_fn=None, | ||
# empty_ok=False, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably best to remove the commented line and trailing comment character |
||
**kwargs): | ||
""" | ||
:param param_decls: relayed to :class:`click.Option` | ||
:param switch: sequence of parameter names | ||
:param switch: sequence of parameter | ||
:param prompt_fn: a callable that defines if the option should be asked for or not in interactive mode | ||
""" | ||
# intercept prompt kwarg | ||
|
||
# intercept prompt kwarg; I need to pop it before calling super | ||
self._prompt = kwargs.pop('prompt', None) | ||
if kwargs.get('required', None): | ||
required_fn = kwargs.get('required_fn', lambda ctx: True) | ||
kwargs['required_fn'] = lambda ctx: noninteractive(ctx) and required_fn(ctx) | ||
|
||
# super | ||
# call super | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If important to explain why we call super and why here than maybe we should write that as a comment. As it stands it is not very informative and I would remove it |
||
super(InteractiveOption, self).__init__(param_decls=param_decls, **kwargs) | ||
|
||
self.prompt_fn = prompt_fn | ||
|
||
# I check that a prompt was actually defined. | ||
# I do it after calling super so e.g. 'self.name' is defined | ||
if not self._prompt: | ||
raise TypeError( | ||
"Interactive options need to have a prompt specified, but '{}' does not have a prompt defined".format( | ||
self.name)) | ||
|
||
# other kwargs | ||
self.switch = switch | ||
self.empty_ok = empty_ok | ||
|
||
# set callback | ||
if self._prompt: | ||
self._after_callback = self.callback | ||
self.callback = self.prompt_callback | ||
self._after_callback = self.callback | ||
self.callback = self.prompt_callback | ||
|
||
# set controll strings that trigger special features from the input prompt | ||
# set control strings that trigger special features from the input prompt | ||
self._ctrl = {'?': self.ctrl_help} | ||
|
||
# set prompting type | ||
|
@@ -108,13 +120,6 @@ def format_help_message(self): | |
msg = click.style('\t' + msg, fg='green') | ||
return msg | ||
|
||
def unacceptably_empty(self, value): | ||
"""check if the value is empty and should not be passed on to conversion""" | ||
result = not value and not isinstance(value, bool) | ||
if self.empty_ok: | ||
return False | ||
return result | ||
|
||
def full_process_value(self, ctx, value): | ||
""" | ||
catch errors raised by ConditionalOption in order to adress them in | ||
|
@@ -146,10 +151,8 @@ def simple_prompt_loop(self, ctx, param, value): | |
# prompt | ||
value = self.prompt_func(ctx) | ||
if value in self._ctrl: | ||
# dispatch | ||
# dispatch - e.g. show help | ||
self._ctrl[value]() | ||
elif self.unacceptably_empty(value): | ||
# repeat prompting without trying to convert | ||
continue | ||
else: | ||
# try to convert, if unsuccessful continue prompting | ||
|
@@ -166,42 +169,40 @@ def after_callback(self, ctx, param, value): | |
def prompt_callback(self, ctx, param, value): | ||
"""decide wether to initiate the prompt_loop or not""" | ||
|
||
# a value was given | ||
# a value was given on the command line: then just go with validation | ||
if value is not None: | ||
return self.after_callback(ctx, param, value) | ||
|
||
# parameter is not reqired anyway | ||
if not self.is_required(ctx): | ||
return self.after_callback(ctx, param, value) | ||
# The same if the user specified --non-interactive | ||
if noninteractive(ctx): | ||
# Check if it is required | ||
|
||
# help option was passed on the cmdline | ||
if ctx.params.get('help'): | ||
return self.after_callback(ctx, param, value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it tested somewhere that removing this never causes an InteractiveOption to enter the prompt loop before There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it wasn't tested before, then no. It wasn't clear to me the purpose of that logic. Please feel free to add a test for what you have in mind. |
||
default = self._get_default(ctx) or self.default | ||
|
||
# no value was given | ||
try: | ||
# try to convert None | ||
value = self.after_callback(ctx, param, self.type.convert('', param, ctx)) | ||
# if conversion comes up empty, make sure empty is acceptable | ||
if self.unacceptably_empty(value): | ||
raise click.MissingParameter(param=param) | ||
|
||
except (click.MissingParameter, click.BadParameter): | ||
# no value was given but a value is required | ||
# check for BadParameter too, because convert might not check for None specifically | ||
|
||
# no prompting allowed | ||
if noninteractive(ctx): | ||
# either get a default value and return ... | ||
default = self._get_default(ctx) or self.default | ||
if default is not None: | ||
return self.type.convert(default, param, ctx) | ||
else: | ||
# ... or raise Missing Parameter | ||
if default is not None: | ||
# There is a default | ||
value = self.type.convert(default, param, ctx) | ||
else: | ||
# There is no default. | ||
# If required | ||
if self.is_required(ctx): | ||
raise click.MissingParameter() | ||
# prompting allowed | ||
# In the else case: no default, not required: value is None, it's just passed to the after_callback | ||
return self.after_callback(ctx, param, value) | ||
|
||
if self.prompt_fn is None or (self.prompt_fn is not None and self.prompt_fn(ctx)): | ||
# There is no prompt_fn function, or a prompt_fn function and it says we should ask for the value | ||
|
||
# If we are here, we are in interactive mode and the parameter is not specified | ||
# We enter the prompt loop | ||
value = self.prompt_loop(ctx, param, value) | ||
return value | ||
else: | ||
# There is a prompt_fn function and returns False (i.e. should not ask for this value | ||
# We then set the value to None | ||
value = None | ||
|
||
# And then we call the callback | ||
return self.after_callback(ctx, param, value) | ||
|
||
|
||
def opt_prompter(ctx, cmd, givenkwargs, oldvalues=None): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since time is of the essence for unit tests, what about
sleep 0.1
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately the time precision on my Mac is 1 second, so a time >=1s is needed... Of course better approaches are accepted (even if probably they should become fixes to
click.edit()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made a PR pallets/click#1050 at click to fix this.
Once merged and released we can
sleep 1
vim
withsed
(slimmer, no "window" open, ...) as I did in that PRThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vim was chosen because it is a file editor and therefore closer to the intended usage than the stream editor sed. Since this test turned out to be fragile, I agree that maybe sed is a better compromise between test reliability and testing the right thing.