2929from ._type_checking import ActionsContainer , ArgumentParser , docstring_parser
3030
3131__all__ = [
32+ "Unset" ,
3233 "set_parsing_settings" ,
3334]
3435
4243capture_typing_extension_shadows (_UnpackGenericAlias , "_UnpackGenericAlias" , unpack_meta_types )
4344
4445
46+ class _UnsetType :
47+ """Sentinel class for unset argument values."""
48+
49+ _instance = None
50+ _SERIALIZED = "==UNSET=="
51+
52+ def __new__ (cls ):
53+ if cls ._instance is None :
54+ cls ._instance = super ().__new__ (cls )
55+ return cls ._instance
56+
57+ def __repr__ (self ):
58+ return "Unset"
59+
60+ def __bool__ (self ):
61+ return False
62+
63+
64+ Unset = _UnsetType ()
65+
66+
4567class InstantiatorCallable (Protocol ):
4668 def __call__ (self , class_type : type [ClassType ], * args , ** kwargs ) -> ClassType :
4769 pass # pragma: no cover
@@ -94,12 +116,13 @@ def parser_context(**kwargs):
94116 context_var .reset (token )
95117
96118
97- parsing_settings = {
119+ parsing_settings : dict = {
98120 "validate_defaults" : False ,
99121 "parse_optionals_as_positionals" : False ,
100122 "add_print_completion_argument" : False ,
101123 "stubs_resolver_allow_py_files" : False ,
102124 "omegaconf_absolute_to_relative_paths" : False ,
125+ "unset_sentinel" : None ,
103126}
104127
105128
@@ -122,6 +145,7 @@ def set_parsing_settings(
122145 add_print_completion_argument : bool | None = None ,
123146 stubs_resolver_allow_py_files : bool | None = None ,
124147 omegaconf_absolute_to_relative_paths : bool | None = None ,
148+ unset_sentinel : bool | None = None ,
125149 subclasses_disabled : list [type | Callable [[type ], bool ]] | None = None ,
126150 subclasses_enabled : list [type | str ] | None = None ,
127151) -> None :
@@ -155,6 +179,12 @@ def set_parsing_settings(
155179 with ``omegaconf+`` parser mode, absolute interpolation paths are
156180 converted to relative. This is only intended for backward
157181 compatibility with ``omegaconf`` parser mode.
182+ unset_sentinel: If ``True``, parsers will use the :obj:`.Unset` sentinel
183+ for arguments that have not been given a value (instead of
184+ ``None``). This allows distinguishing between ``None`` as an
185+ explicitly given value and an argument that was not provided at
186+ all. If ``False``, uses ``None`` (the default, argparse-compatible
187+ behavior) unless overridden by ``argument_default``.
158188 subclasses_disabled: List of types or functions, so that when parsing
159189 only the exact type hints (not their subclasses) are accepted.
160190 Descendants of the configured types are also disabled. Functions
@@ -203,6 +233,11 @@ def set_parsing_settings(
203233 raise ValueError (
204234 f"omegaconf_absolute_to_relative_paths must be a boolean, but got { omegaconf_absolute_to_relative_paths } ."
205235 )
236+ # unset_sentinel
237+ if isinstance (unset_sentinel , bool ):
238+ parsing_settings ["unset_sentinel" ] = Unset if unset_sentinel else None
239+ elif unset_sentinel is not None :
240+ raise ValueError (f"unset_sentinel must be a boolean, but got { unset_sentinel } ." )
206241 # subclass behavior
207242 if subclasses_disabled or subclasses_enabled :
208243 subclass_type_behavior (
@@ -222,7 +257,11 @@ def get_parsing_setting(name: str):
222257
223258
224259def validate_default (container : ActionsContainer , action : argparse .Action ):
225- if action .default is None or not get_parsing_setting ("validate_defaults" ) or not hasattr (action , "_check_type" ):
260+ if (
261+ action .default is get_parsing_setting ("unset_sentinel" )
262+ or not get_parsing_setting ("validate_defaults" )
263+ or not hasattr (action , "_check_type" )
264+ ):
226265 return
227266 try :
228267 from ._core import ArgumentGroup
0 commit comments