@@ -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,7 +163,6 @@ 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 if sys .version_info < (3 , 12 ):
169168 # Older argparse have a bugged parse_known_intermixed_args.
@@ -175,7 +174,7 @@ def parse_known_and_unknown_args(
175174 (unknown_flags if arg .startswith ("-" ) else file_or_dir ).append (arg )
176175 return namespace , unknown_flags
177176 else :
178- return optparser .parse_known_intermixed_args (strargs , namespace )
177+ return self . optparser .parse_known_intermixed_args (strargs , namespace )
179178
180179 def addini (
181180 self ,
@@ -392,15 +391,14 @@ class OptionGroup:
392391
393392 def __init__ (
394393 self ,
394+ arggroup : argparse ._ArgumentGroup ,
395395 name : str ,
396- description : str = "" ,
397- parser : Parser | None = None ,
398- * ,
396+ parser : Parser | None ,
399397 _ispytest : bool = False ,
400398 ) -> None :
401399 check_ispytest (_ispytest )
400+ self ._arggroup = arggroup
402401 self .name = name
403- self .description = description
404402 self .options : list [Argument ] = []
405403 self .parser = parser
406404
@@ -435,30 +433,32 @@ def _addoption_instance(self, option: Argument, shortupper: bool = False) -> Non
435433 for opt in option ._short_opts :
436434 if opt [0 ] == "-" and opt [1 ].islower ():
437435 raise ValueError ("lowercase shortoptions reserved" )
436+
438437 if self .parser :
439438 self .parser .processoption (option )
439+
440+ self ._arggroup .add_argument (* option .names (), ** option .attrs ())
440441 self .options .append (option )
441442
442443
443444class PytestArgumentParser (argparse .ArgumentParser ):
444445 def __init__ (
445446 self ,
446447 parser : Parser ,
447- extra_info : dict [ str , Any ] | None = None ,
448- prog : str | None = None ,
448+ usage : str | None ,
449+ extra_info : dict [ str , str ] ,
449450 ) -> None :
450451 self ._parser = parser
451452 super ().__init__ (
452- prog = prog ,
453- usage = parser ._usage ,
453+ usage = usage ,
454454 add_help = False ,
455455 formatter_class = DropShorterLongHelpFormatter ,
456456 allow_abbrev = False ,
457457 fromfile_prefix_chars = "@" ,
458458 )
459459 # extra_info is a dict of (param -> value) to display if there's
460460 # an usage error to provide more contextual information to the user.
461- self .extra_info = extra_info if extra_info else {}
461+ self .extra_info = extra_info
462462
463463 def error (self , message : str ) -> NoReturn :
464464 """Transform argparse error message into UsageError."""
0 commit comments