@@ -36,8 +36,6 @@ class Parser:
3636 there's an error processing the command line arguments.
3737 """
3838
39- prog : str | None = None
40-
4139 def __init__ (
4240 self ,
4341 usage : str | None = None ,
@@ -46,14 +44,31 @@ def __init__(
4644 _ispytest : bool = False ,
4745 ) -> None :
4846 check_ispytest (_ispytest )
49- self ._anonymous = OptionGroup ("Custom options" , parser = self , _ispytest = True )
50- self ._groups : list [OptionGroup ] = []
47+
48+ from _pytest ._argcomplete import filescompleter
49+
5150 self ._processopt = processopt
52- self ._usage = usage
51+ self .extra_info : dict [str , Any ] = {}
52+ self .optparser = PytestArgumentParser (self , usage , self .extra_info )
53+ anonymous_arggroup = self .optparser .add_argument_group ("Custom options" )
54+ self ._anonymous = OptionGroup (
55+ anonymous_arggroup , "_anonymous" , self , _ispytest = True
56+ )
57+ self ._groups = [self ._anonymous ]
58+ file_or_dir_arg = self .optparser .add_argument (FILE_OR_DIR , nargs = "*" )
59+ file_or_dir_arg .completer = filescompleter # type: ignore
60+
5361 self ._inidict : dict [str , tuple [str , str , Any ]] = {}
5462 # Maps alias -> canonical name.
5563 self ._ini_aliases : dict [str , str ] = {}
56- self .extra_info : dict [str , Any ] = {}
64+
65+ @property
66+ def prog (self ) -> str :
67+ return self .optparser .prog
68+
69+ @prog .setter
70+ def prog (self , value : str ) -> None :
71+ self .optparser .prog = value
5772
5873 def processoption (self , option : Argument ) -> None :
5974 if self ._processopt :
@@ -78,12 +93,17 @@ def getgroup(
7893 for group in self ._groups :
7994 if group .name == name :
8095 return group
81- group = OptionGroup (name , description , parser = self , _ispytest = True )
96+
97+ arggroup = self .optparser .add_argument_group (description or name )
98+ group = OptionGroup (arggroup , name , self , _ispytest = True )
8299 i = 0
83100 for i , grp in enumerate (self ._groups ):
84101 if grp .name == after :
85102 break
86103 self ._groups .insert (i + 1 , group )
104+ # argparse doesn't provide a way to control `--help` order, so must
105+ # access its internals ☹.
106+ self .optparser ._action_groups .insert (i + 1 , self .optparser ._action_groups .pop ())
87107 return group
88108
89109 def addoption (self , * opts : str , ** attrs : Any ) -> None :
@@ -102,25 +122,6 @@ def addoption(self, *opts: str, **attrs: Any) -> None:
102122 """
103123 self ._anonymous .addoption (* opts , ** attrs )
104124
105- def _getparser (self ) -> PytestArgumentParser :
106- from _pytest ._argcomplete import filescompleter
107-
108- optparser = PytestArgumentParser (self , self .extra_info , prog = self .prog )
109- groups = [* self ._groups , self ._anonymous ]
110- for group in groups :
111- if group .options :
112- desc = group .description or group .name
113- arggroup = optparser .add_argument_group (desc )
114- for option in group .options :
115- n = option .names ()
116- a = option .attrs ()
117- arggroup .add_argument (* n , ** a )
118- file_or_dir_arg = optparser .add_argument (FILE_OR_DIR , nargs = "*" )
119- # bash like autocompletion for dirs (appending '/')
120- # Type ignored because typeshed doesn't know about argcomplete.
121- file_or_dir_arg .completer = filescompleter # type: ignore
122- return optparser
123-
124125 def parse (
125126 self ,
126127 args : Sequence [str | os .PathLike [str ]],
@@ -135,7 +136,6 @@ def parse(
135136 """
136137 from _pytest ._argcomplete import try_argcomplete
137138
138- self .optparser = self ._getparser ()
139139 try_argcomplete (self .optparser )
140140 strargs = [os .fspath (x ) for x in args ]
141141 return self .optparser .parse_intermixed_args (strargs , namespace = namespace )
@@ -163,13 +163,12 @@ def parse_known_and_unknown_args(
163163 A tuple containing an argparse namespace object for the known
164164 arguments, and a list of unknown flag arguments.
165165 """
166- optparser = self ._getparser ()
167166 strargs = [os .fspath (x ) for x in args ]
168167 # Workaround `UserWarning: Do not expect file_or_dir in Namespace` in older argparse.
169168 if sys .version_info < (3 , 12 ):
170169 if namespace is not None and getattr (namespace , FILE_OR_DIR , None ) == []:
171170 delattr (namespace , FILE_OR_DIR )
172- return optparser .parse_known_intermixed_args (strargs , namespace = namespace )
171+ return self . optparser .parse_known_intermixed_args (strargs , namespace = namespace )
173172
174173 def addini (
175174 self ,
@@ -386,15 +385,14 @@ class OptionGroup:
386385
387386 def __init__ (
388387 self ,
388+ arggroup : argparse ._ArgumentGroup ,
389389 name : str ,
390- description : str = "" ,
391- parser : Parser | None = None ,
392- * ,
390+ parser : Parser | None ,
393391 _ispytest : bool = False ,
394392 ) -> None :
395393 check_ispytest (_ispytest )
394+ self ._arggroup = arggroup
396395 self .name = name
397- self .description = description
398396 self .options : list [Argument ] = []
399397 self .parser = parser
400398
@@ -429,30 +427,32 @@ def _addoption_instance(self, option: Argument, shortupper: bool = False) -> Non
429427 for opt in option ._short_opts :
430428 if opt [0 ] == "-" and opt [1 ].islower ():
431429 raise ValueError ("lowercase shortoptions reserved" )
430+
432431 if self .parser :
433432 self .parser .processoption (option )
433+
434+ self ._arggroup .add_argument (* option .names (), ** option .attrs ())
434435 self .options .append (option )
435436
436437
437438class PytestArgumentParser (argparse .ArgumentParser ):
438439 def __init__ (
439440 self ,
440441 parser : Parser ,
441- extra_info : dict [ str , Any ] | None = None ,
442- prog : str | None = None ,
442+ usage : str | None ,
443+ extra_info : dict [ str , str ] ,
443444 ) -> None :
444445 self ._parser = parser
445446 super ().__init__ (
446- prog = prog ,
447- usage = parser ._usage ,
447+ usage = usage ,
448448 add_help = False ,
449449 formatter_class = DropShorterLongHelpFormatter ,
450450 allow_abbrev = False ,
451451 fromfile_prefix_chars = "@" ,
452452 )
453453 # extra_info is a dict of (param -> value) to display if there's
454454 # an usage error to provide more contextual information to the user.
455- self .extra_info = extra_info if extra_info else {}
455+ self .extra_info = extra_info
456456
457457 def error (self , message : str ) -> NoReturn :
458458 """Transform argparse error message into UsageError."""
0 commit comments