@@ -1406,6 +1406,11 @@ def test_type_from_flag_value():
14061406 assert param .type is click .INT
14071407 param = click .Option (["-b" , "x" ], flag_value = 8 )
14081408 assert param .type is click .INT
1409+ # Non-basic types auto-detect as UNPROCESSED to avoid stringification.
1410+ param = click .Option (["-c" , "x" ], flag_value = EngineType .OSS )
1411+ assert param .type is click .UNPROCESSED
1412+ param = click .Option (["-d" , "x" ], flag_value = frozenset ())
1413+ assert param .type is click .UNPROCESSED
14091414
14101415
14111416@pytest .mark .parametrize (
@@ -2127,13 +2132,13 @@ class Class2:
21272132 [],
21282133 EngineType .OSS ,
21292134 ),
2130- # Type is not specified and default to string, so the default value is
2131- # returned as a string, even if it is a boolean. Also, defaults to the
2132- # flag_value instead of the default value to support legacy behavior .
2135+ # Type is not specified. For string flag_value, STRING type is used and
2136+ # the default value is converted to string. For non-basic types (like
2137+ # enums), UNPROCESSED is used and values pass through unchanged .
21332138 ({"flag_value" : "1" , "default" : True }, [], "1" ),
21342139 ({"flag_value" : "1" , "default" : 42 }, [], "42" ),
2135- ({"flag_value" : EngineType .OSS , "default" : True }, [], " EngineType.OSS" ),
2136- ({"flag_value" : EngineType .OSS , "default" : 42 }, [], "42" ),
2140+ ({"flag_value" : EngineType .OSS , "default" : True }, [], EngineType .OSS ),
2141+ ({"flag_value" : EngineType .OSS , "default" : 42 }, [], 42 ),
21372142 # See: the result is the same if we force the type to be str.
21382143 ({"type" : str , "flag_value" : 1 , "default" : True }, [], "1" ),
21392144 ({"type" : str , "flag_value" : 1 , "default" : 42 }, [], "42" ),
@@ -2199,28 +2204,29 @@ def scan(pro):
21992204 ["--opt2" ],
22002205 EngineType .PRO ,
22012206 ),
2202- # Check that passing exotic flag values like classes is supported, but are
2203- # rendered to strings when the type is not specified.
2207+ # Exotic flag values like classes are passed through unchanged when no
2208+ # explicit type is given (UNPROCESSED is auto-detected).
2209+ # https://github.com/pallets/click/issues/2012
22042210 # https://github.com/pallets/click/issues/3121
22052211 (
22062212 {"flag_value" : Class1 , "default" : True },
22072213 {"flag_value" : Class2 },
22082214 [],
2209- "<class 'test_options. Class1'>" ,
2215+ Class1 ,
22102216 ),
22112217 (
22122218 {"flag_value" : Class1 , "default" : True },
22132219 {"flag_value" : Class2 },
22142220 ["--opt1" ],
2215- "<class 'test_options. Class1'>" ,
2221+ Class1 ,
22162222 ),
22172223 (
22182224 {"flag_value" : Class1 , "default" : True },
22192225 {"flag_value" : Class2 },
22202226 ["--opt2" ],
2221- "<class 'test_options. Class2'>" ,
2227+ Class2 ,
22222228 ),
2223- # Even the default is processed as a string .
2229+ # String and None defaults pass through unchanged .
22242230 ({"flag_value" : Class1 , "default" : "True" }, {"flag_value" : Class2 }, [], "True" ),
22252231 ({"flag_value" : Class1 , "default" : None }, {"flag_value" : Class2 }, [], None ),
22262232 # To get the classes as-is, we need to specify the type as UNPROCESSED.
@@ -2245,18 +2251,18 @@ def scan(pro):
22452251 ),
22462252 # Setting the default to a class, an instance of the class is returned instead
22472253 # of the class itself, because the default is allowed to be callable (and
2248- # consummd ). And this happens whatever the type is.
2254+ # consumed ). And this happens whatever the type is.
22492255 (
22502256 {"flag_value" : Class1 , "default" : Class1 },
22512257 {"flag_value" : Class2 },
22522258 [],
2253- re .compile (r"' <test_options.Class1 object at 0x[0-9A-Fa-f]+>' " ),
2259+ re .compile (r"<test_options.Class1 object at 0x[0-9A-Fa-f]+>" ),
22542260 ),
22552261 (
22562262 {"flag_value" : Class1 , "default" : Class2 },
22572263 {"flag_value" : Class2 },
22582264 [],
2259- re .compile (r"' <test_options.Class2 object at 0x[0-9A-Fa-f]+>' " ),
2265+ re .compile (r"<test_options.Class2 object at 0x[0-9A-Fa-f]+>" ),
22602266 ),
22612267 (
22622268 {"flag_value" : Class1 , "type" : UNPROCESSED , "default" : Class1 },
@@ -2322,12 +2328,13 @@ def cli(dual_option):
23222328 ["--opt" ],
23232329 Class1 ,
23242330 ),
2325- # Without UNPROCESSED, the class is str()-ified by the default STRING type.
2326- ({"flag_value" : Class1 , "default" : True }, [], "<class 'test_options.Class1'>" ),
2331+ # Without explicit UNPROCESSED, the class still passes through unchanged
2332+ # because UNPROCESSED is auto-detected for non-basic flag_value types.
2333+ ({"flag_value" : Class1 , "default" : True }, [], Class1 ),
23272334 (
23282335 {"flag_value" : Class1 , "default" : True },
23292336 ["--opt" ],
2330- "<class 'test_options. Class1'>" ,
2337+ Class1 ,
23312338 ),
23322339 # Explicit default=Class1 (not via default=True alignment): callable IS invoked,
23332340 # because the user explicitly set a callable as the default.
@@ -2473,6 +2480,48 @@ def cli(value):
24732480 assert opt .get_default (ctx , call = True ) is expected_get_default
24742481
24752482
2483+ def test_flag_value_not_stringified_for_custom_types (runner ):
2484+ """Non-basic flag_value types are passed through unchanged without
2485+ requiring ``type=click.UNPROCESSED``.
2486+
2487+ Regression test for https://github.com/pallets/click/issues/2012
2488+ """
2489+
2490+ @click .command ()
2491+ @click .option ("--cls1" , "config_cls" , flag_value = Class1 , default = True )
2492+ @click .option ("--cls2" , "config_cls" , flag_value = Class2 )
2493+ def cli (config_cls ):
2494+ click .echo (repr (config_cls ), nl = False )
2495+
2496+ # Default activates --cls1 (default=True resolves to flag_value).
2497+ result = runner .invoke (cli , [])
2498+ assert result .exit_code == 0
2499+ assert result .output == repr (Class1 )
2500+
2501+ result = runner .invoke (cli , ["--cls1" ])
2502+ assert result .exit_code == 0
2503+ assert result .output == repr (Class1 )
2504+
2505+ result = runner .invoke (cli , ["--cls2" ])
2506+ assert result .exit_code == 0
2507+ assert result .output == repr (Class2 )
2508+
2509+ # Enum flag_value without explicit type is also preserved.
2510+ @click .command ()
2511+ @click .option ("--oss" , "engine" , flag_value = EngineType .OSS , default = True )
2512+ @click .option ("--pro" , "engine" , flag_value = EngineType .PRO )
2513+ def cli2 (engine ):
2514+ click .echo (repr (engine ), nl = False )
2515+
2516+ result = runner .invoke (cli2 , [])
2517+ assert result .exit_code == 0
2518+ assert result .output == repr (EngineType .OSS )
2519+
2520+ result = runner .invoke (cli2 , ["--pro" ])
2521+ assert result .exit_code == 0
2522+ assert result .output == repr (EngineType .PRO )
2523+
2524+
24762525def test_custom_type_frozenset_flag_value (runner ):
24772526 """Check that frozenset is correctly handled as a type, a flag value and a default.
24782527
0 commit comments