11import re
22from dataclasses import dataclass
3- from typing import Literal , Optional
3+ from typing import List , Literal , Optional
44from unittest .mock import patch
55
66import pytest
@@ -254,6 +254,14 @@ def test_unset_sentinel_default_is_none(parser):
254254 assert get_parsing_setting ("unset_sentinel" ) is None
255255
256256
257+ def test_unset_sentinel_get_defaults (parser ):
258+ set_parsing_settings (unset_sentinel = True )
259+
260+ parser .add_argument ("--num" , type = int )
261+ defaults = parser .get_defaults ()
262+ assert defaults .num is Unset
263+
264+
257265def test_unset_sentinel_optional_int (parser ):
258266 set_parsing_settings (unset_sentinel = True )
259267
@@ -281,15 +289,7 @@ def test_unset_sentinel_explicit_none_default_stays_none(parser):
281289 assert defaults .num is None
282290
283291
284- def test_unset_sentinel_get_defaults (parser ):
285- set_parsing_settings (unset_sentinel = True )
286-
287- parser .add_argument ("--num" , type = int )
288- defaults = parser .get_defaults ()
289- assert defaults .num is Unset
290-
291-
292- def test_unset_sentinel_skip_unset_dump (parser ):
292+ def test_unset_sentinel_dump_skip_unset (parser ):
293293 set_parsing_settings (unset_sentinel = True )
294294
295295 parser .add_argument ("--num" , type = Optional [int ])
@@ -306,6 +306,30 @@ def test_unset_sentinel_skip_unset_dump(parser):
306306 assert json_or_yaml_load (dump_no_skip ) == {"num" : "==UNSET==" , "name" : "hello" }
307307
308308
309+ @dataclass
310+ class UnsetListItem :
311+ name : str
312+ value : Optional [int ]
313+
314+
315+ def test_unset_sentinel_dump_list_of_dataclasses (parser ):
316+ set_parsing_settings (unset_sentinel = True )
317+
318+ parser .add_argument ("--records" , type = List [UnsetListItem ])
319+
320+ cfg = parser .parse_args (['--records=[{"name":"a"},{"name":"b","value":2}]' ])
321+ assert cfg .records [0 ] == Namespace (name = "a" , value = Unset )
322+ assert cfg .records [1 ] == Namespace (name = "b" , value = 2 )
323+
324+ dump_skip = parser .dump (cfg , skip_unset = True )
325+ loaded_skip = json_or_yaml_load (dump_skip )
326+ assert loaded_skip == {"records" : [{"name" : "a" }, {"name" : "b" , "value" : 2 }]}
327+
328+ dump_no_skip = parser .dump (cfg , skip_unset = False )
329+ loaded_no_skip = json_or_yaml_load (dump_no_skip )
330+ assert loaded_no_skip == {"records" : [{"name" : "a" , "value" : "==UNSET==" }, {"name" : "b" , "value" : 2 }]}
331+
332+
309333def test_unset_sentinel_validate_skip_unset (parser ):
310334 set_parsing_settings (unset_sentinel = True )
311335
@@ -333,13 +357,51 @@ def test_unset_repr():
333357
334358
335359def test_unset_bool ():
336- assert not Unset
360+ assert bool (Unset ) is False
361+ assert bool (not Unset ) is True
337362
338363
339364def test_unset_is_singleton ():
340365 assert _UnsetType () is Unset
341366
342367
368+ # add_function_arguments with unset_sentinel
369+
370+
371+ def test_unset_sentinel_function_arguments_no_default_is_unset (parser ):
372+ """Optional param with no default in function signature should be Unset when unset_sentinel=True."""
373+ set_parsing_settings (unset_sentinel = True )
374+
375+ def my_func (num : Optional [int ], name : str = "hello" ):
376+ pass # pragma: no cover
377+
378+ parser .add_function_arguments (my_func )
379+
380+ cfg = parser .parse_args ([])
381+ assert cfg .num is Unset # no default in signature → Unset
382+ assert cfg .name == "hello" # has default → kept
383+
384+ cfg = parser .parse_args (["--num=null" ])
385+ assert cfg .num is None # explicitly set to null → None
386+
387+ cfg = parser .parse_args (["--num=5" ])
388+ assert cfg .num == 5
389+
390+
391+ def test_unset_sentinel_function_arguments_explicit_none_default_stays_none (parser ):
392+ """Optional param with explicit default=None in function signature stays None."""
393+ set_parsing_settings (unset_sentinel = True )
394+
395+ def my_func (num : Optional [int ] = None , flag : Optional [int ] = 0 ):
396+ pass # pragma: no cover
397+
398+ parser .add_function_arguments (my_func )
399+
400+ cfg = parser .parse_args ([])
401+ assert cfg .num is None # explicit default=None → None (not Unset)
402+ assert cfg .flag == 0 # explicit default=0 → 0
403+
404+
343405# Interaction between Unset and default SUPPRESS
344406
345407
@@ -377,3 +439,17 @@ def test_unset_and_argument_default_suppress(parser):
377439 cfg = parser .parse_args (["--num=7" ])
378440 assert cfg .num == 7
379441 assert not hasattr (cfg , "name" ) # still absent
442+
443+
444+ def test_unset_parse_and_print_config (parser ):
445+ set_parsing_settings (unset_sentinel = True )
446+
447+ parser .add_argument ("--num" , type = int )
448+ parser .add_argument ("--name" , type = str , default = "a" )
449+ parser .add_argument ("--cfg" , action = "config" )
450+
451+ cfg = parser .parse_args (["--cfg={}" ])
452+ assert cfg == Namespace (num = Unset , name = "a" , cfg = [None ])
453+
454+ out = get_parse_args_stdout (parser , ["--print_config" ])
455+ assert json_or_yaml_load (out ) == {"num" : "==UNSET==" , "name" : "a" }
0 commit comments