@@ -181,6 +181,116 @@ def test_make_pass_meta_decorator_doc():
181181 assert "passes the test value" in pass_value .__doc__
182182
183183
184+ def test_hiding_of_unset_sentinel_in_callbacks ():
185+ """Fix: https://github.com/pallets/click/issues/3136"""
186+
187+ def inspect_other_params (ctx , param , value ):
188+ """A callback that inspects other parameters' values via the context."""
189+ assert click .get_current_context () is ctx
190+ click .echo (f"callback.my_arg: { ctx .params .get ('my_arg' )!r} " )
191+ click .echo (f"callback.my_opt: { ctx .params .get ('my_opt' )!r} " )
192+ click .echo (f"callback.my_callback: { ctx .params .get ('my_callback' )!r} " )
193+
194+ click .echo (f"callback.param: { param !r} " )
195+ click .echo (f"callback.value: { value !r} " )
196+
197+ return "hard-coded"
198+
199+ class ParameterInternalCheck (Option ):
200+ """An option that checks internal state during processing."""
201+
202+ def process_value (self , ctx , value ):
203+ """Check that UNSET values are hidden as None in ctx.params within the
204+ callback, and then properly restored afterwards.
205+ """
206+ assert click .get_current_context () is ctx
207+ click .echo (f"before_process.my_arg: { ctx .params .get ('my_arg' )!r} " )
208+ click .echo (f"before_process.my_opt: { ctx .params .get ('my_opt' )!r} " )
209+ click .echo (f"before_process.my_callback: { ctx .params .get ('my_callback' )!r} " )
210+
211+ value = super ().process_value (ctx , value )
212+
213+ assert click .get_current_context () is ctx
214+ click .echo (f"after_process.my_arg: { ctx .params .get ('my_arg' )!r} " )
215+ click .echo (f"after_process.my_opt: { ctx .params .get ('my_opt' )!r} " )
216+ click .echo (f"after_process.my_callback: { ctx .params .get ('my_callback' )!r} " )
217+
218+ return value
219+
220+ def change_other_params (ctx , param , value ):
221+ """A callback that modifies other parameters' values via the context."""
222+ assert click .get_current_context () is ctx
223+ click .echo (f"before_change.my_arg: { ctx .params .get ('my_arg' )!r} " )
224+ click .echo (f"before_change.my_opt: { ctx .params .get ('my_opt' )!r} " )
225+ click .echo (f"before_change.my_callback: { ctx .params .get ('my_callback' )!r} " )
226+
227+ click .echo (f"before_change.param: { param !r} " )
228+ click .echo (f"before_change.value: { value !r} " )
229+
230+ ctx .params ["my_arg" ] = "changed"
231+ # Reset to None parameters that where not UNSET to see they are not forced back
232+ # to UNSET.
233+ ctx .params ["my_callback" ] = None
234+
235+ return value
236+
237+ @click .command
238+ @click .argument ("my-arg" , required = False )
239+ @click .option ("--my-opt" )
240+ @click .option ("--my-callback" , callback = inspect_other_params )
241+ @click .option ("--check-internal" , cls = ParameterInternalCheck )
242+ @click .option (
243+ "--modifying-callback" , cls = ParameterInternalCheck , callback = change_other_params
244+ )
245+ @click .pass_context
246+ def cli (ctx , my_arg , my_opt , my_callback , check_internal , modifying_callback ):
247+ click .echo (f"cli.my_arg: { my_arg !r} " )
248+ click .echo (f"cli.my_opt: { my_opt !r} " )
249+ click .echo (f"cli.my_callback: { my_callback !r} " )
250+ click .echo (f"cli.check_internal: { check_internal !r} " )
251+ click .echo (f"cli.modifying_callback: { modifying_callback !r} " )
252+
253+ runner = click .testing .CliRunner ()
254+ result = runner .invoke (cli )
255+
256+ assert result .stdout .splitlines () == [
257+ # Values of other parameters within the callback are None, not UNSET.
258+ "callback.my_arg: None" ,
259+ "callback.my_opt: None" ,
260+ "callback.my_callback: None" ,
261+ "callback.param: <Option my_callback>" ,
262+ "callback.value: None" ,
263+ # Previous UNSET values were not altered by the callback.
264+ "before_process.my_arg: Sentinel.UNSET" ,
265+ "before_process.my_opt: Sentinel.UNSET" ,
266+ "before_process.my_callback: 'hard-coded'" ,
267+ "after_process.my_arg: Sentinel.UNSET" ,
268+ "after_process.my_opt: Sentinel.UNSET" ,
269+ "after_process.my_callback: 'hard-coded'" ,
270+ # Changes on other parameters within the callback are restored afterwards.
271+ "before_process.my_arg: Sentinel.UNSET" ,
272+ "before_process.my_opt: Sentinel.UNSET" ,
273+ "before_process.my_callback: 'hard-coded'" ,
274+ "before_change.my_arg: None" ,
275+ "before_change.my_opt: None" ,
276+ "before_change.my_callback: 'hard-coded'" ,
277+ "before_change.param: <ParameterInternalCheck modifying_callback>" ,
278+ "before_change.value: None" ,
279+ "after_process.my_arg: 'changed'" ,
280+ "after_process.my_opt: Sentinel.UNSET" ,
281+ "after_process.my_callback: None" ,
282+ # Unset values within the main command are UNSET, but hidden as None.
283+ "cli.my_arg: 'changed'" ,
284+ "cli.my_opt: None" ,
285+ "cli.my_callback: None" ,
286+ "cli.check_internal: None" ,
287+ "cli.modifying_callback: None" ,
288+ ]
289+ assert not result .stderr
290+ assert not result .exception
291+ assert result .exit_code == 0
292+
293+
184294def test_context_pushing ():
185295 rv = []
186296
0 commit comments