@@ -356,7 +356,9 @@ def set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
356356# Allow developers to add custom action attributes
357357############################################################################################################
358358
359- CUSTOM_ACTION_ATTRIBS : set [str ] = set ()
359+ # This set should only be edited by calling register_argparse_argument_parameter().
360+ # Do not manually add or remove items.
361+ _CUSTOM_ACTION_ATTRIBS : set [str ] = set ()
360362
361363
362364def register_argparse_argument_parameter (
@@ -369,19 +371,35 @@ def register_argparse_argument_parameter(
369371 :param param_name: Name of the parameter. This must be a valid Python identifier.
370372 :param validator: Optional function to validate and/or transform the parameter value.
371373 It accepts the Action instance and the value as arguments.
374+ :raises ValueError: if the parameter name is invalid
375+ :raises KeyError: if the new parameter collides with any existing attributes
372376 """
373377 if not param_name .isidentifier ():
374- raise KeyError (f"Invalid parameter name '{ param_name } ' - cannot be used as a python identifier" )
378+ raise ValueError (f"Invalid parameter name '{ param_name } ': must be a valid Python identifier" )
375379
380+ if param_name in _CUSTOM_ACTION_ATTRIBS :
381+ raise KeyError (f"Custom parameter '{ param_name } ' is already registered" )
382+
383+ # Ensure we don't hijack standard argparse.Action attributes or existing methods
384+ if hasattr (argparse .Action , param_name ):
385+ raise KeyError (f"'{ param_name } ' conflicts with an existing attribute on argparse.Action" )
386+
387+ # Check if accessors already exist (e.g., from manual patching or previous registration)
388+ getter_name = f'get_{ param_name } '
389+ setter_name = f'set_{ param_name } '
390+ if hasattr (argparse .Action , getter_name ) or hasattr (argparse .Action , setter_name ):
391+ raise KeyError (f"Accessor methods for '{ param_name } ' already exist on argparse.Action" )
392+
393+ # Check for the prefixed internal attribute name collision (e.g., _cmd2_<param_name>)
376394 attr_name = constants .cmd2_attr_name (param_name )
377- if param_name in CUSTOM_ACTION_ATTRIBS or hasattr (argparse .Action , attr_name ):
378- raise KeyError (f"Custom parameter ' { param_name } ' already exists" )
395+ if hasattr (argparse .Action , attr_name ):
396+ raise KeyError (f"The internal attribute ' { attr_name } ' already exists on argparse.Action " )
379397
380398 def _action_get_custom_parameter (self : argparse .Action ) -> Any :
381399 """Get the custom attribute of an argparse Action."""
382400 return getattr (self , attr_name , None )
383401
384- setattr (argparse .Action , f'get_ { param_name } ' , _action_get_custom_parameter )
402+ setattr (argparse .Action , getter_name , _action_get_custom_parameter )
385403
386404 def _action_set_custom_parameter (self : argparse .Action , value : Any ) -> None :
387405 """Set the custom attribute of an argparse Action."""
@@ -390,9 +408,9 @@ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None:
390408
391409 setattr (self , attr_name , value )
392410
393- setattr (argparse .Action , f'set_ { param_name } ' , _action_set_custom_parameter )
411+ setattr (argparse .Action , setter_name , _action_set_custom_parameter )
394412
395- CUSTOM_ACTION_ATTRIBS .add (param_name )
413+ _CUSTOM_ACTION_ATTRIBS .add (param_name )
396414
397415
398416def _validate_completion_callable (self : argparse .Action , value : Any ) -> Any :
@@ -526,7 +544,7 @@ def _ActionsContainer_add_argument( # noqa: N802
526544 kwargs ['nargs' ] = nargs_adjusted
527545
528546 # Extract registered custom keyword arguments
529- custom_attribs = {keyword : value for keyword , value in kwargs .items () if keyword in CUSTOM_ACTION_ATTRIBS }
547+ custom_attribs = {keyword : value for keyword , value in kwargs .items () if keyword in _CUSTOM_ACTION_ATTRIBS }
530548 for keyword in custom_attribs :
531549 del kwargs [keyword ]
532550
0 commit comments