• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mosquito / argclass / 20789187915

07 Jan 2026 04:50PM UTC coverage: 91.909% (-0.005%) from 91.914%
20789187915

Pull #35

github

web-flow
Merge 4eedef094 into 1b66fc0c7
Pull Request #35: handle EnumArgument default properly

302 of 335 branches covered (90.15%)

Branch coverage included in aggregate %.

6 of 8 new or added lines in 2 files covered. (75.0%)

6 existing lines in 3 files now uncovered.

709 of 765 relevant lines covered (92.68%)

4.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.25
/argclass/_parser.py
1
"""Core parser classes for argclass."""
2

3
import ast
5✔
4
import os
5✔
5
from abc import ABCMeta
5✔
6
from argparse import Action, ArgumentError, ArgumentParser
5✔
7
from collections import defaultdict
5✔
8
from enum import EnumMeta
5✔
9
from pathlib import Path
5✔
10
from types import MappingProxyType
5✔
11
from typing import (
5✔
12
    Any,
13
    Dict,
14
    Iterable,
15
    List,
16
    Mapping,
17
    MutableMapping,
18
    NamedTuple,
19
    Optional,
20
    Set,
21
    Tuple,
22
    Type,
23
    TypeVar,
24
    Union,
25
)
26

27
from ._actions import ConfigAction
5✔
28
from ._defaults import AbstractDefaultsParser, INIDefaultsParser
5✔
29
from ._secret import SecretString
5✔
30
from ._store import AbstractGroup, AbstractParser, TypedArgument
5✔
31
from ._types import Actions, Nargs
5✔
32
from ._utils import (
5✔
33
    _unwrap_container_type,
34
    deep_getattr,
35
    merge_annotations,
36
    parse_bool,
37
    unwrap_literal,
38
    unwrap_optional,
39
)
40

41

42
def _make_action_true_argument(
5✔
43
    kind: Type,
44
    default: Any = None,
45
) -> TypedArgument:
46
    """Create a TypedArgument for boolean types."""
47
    kw: Dict[str, Any] = {"type": kind}
5✔
48
    if kind is bool:
5✔
49
        if default is False:
5✔
50
            kw["action"] = Actions.STORE_TRUE
5✔
51
            kw["default"] = False
5✔
52
        elif default is True:
5!
53
            kw["action"] = Actions.STORE_FALSE
5✔
54
            kw["default"] = True
5✔
55
        else:
56
            raise TypeError(f"Can not set default {default!r} for bool")
×
57
    elif kind == Optional[bool]:
5!
58
        kw["action"] = Actions.STORE
5✔
59
        kw["type"] = parse_bool
5✔
60
        kw["default"] = None
5✔
61
    return TypedArgument(**kw)
5✔
62

63

64
def _type_is_bool(kind: Type) -> bool:
5✔
65
    """Check if a type is bool or Optional[bool]."""
66
    return kind is bool or kind == Optional[bool]
5✔
67

68

69
class Meta(ABCMeta):
5✔
70
    """Metaclass for Parser and Group classes."""
71

72
    def __new__(
5✔
73
        mcs,
74
        name: str,
75
        bases: Tuple[Type["Meta"], ...],
76
        attrs: Dict[str, Any],
77
    ) -> "Meta":
78
        # Import here to avoid circular import
79
        from ._factory import EnumArgument
5✔
80

81
        # Create the class first to ensure annotations are available
82
        # Python 3.14+ (PEP 649) defers annotation evaluation, so
83
        # __annotations__ may not be in attrs during class creation
84
        cls = super().__new__(mcs, name, bases, attrs)
5✔
85

86
        # Now get annotations from the created class
87
        annotations = merge_annotations(
5✔
88
            getattr(cls, "__annotations__", {}),
89
            *bases,
90
        )
91

92
        arguments = {}
5✔
93
        argument_groups = {}
5✔
94
        subparsers = {}
5✔
95
        for key, kind in annotations.items():
5✔
96
            if key.startswith("_"):
5✔
97
                continue
5✔
98

99
            try:
5✔
100
                argument = deep_getattr(key, attrs, *bases)
5✔
101
            except KeyError:
5✔
102
                argument = None
5✔
103
                if kind is bool:
5✔
104
                    argument = False
5✔
105

106
            if not isinstance(
5✔
107
                argument,
108
                (TypedArgument, AbstractGroup, AbstractParser),
109
            ):
110
                setattr(cls, key, ...)
5✔
111

112
                is_required = argument is None or argument is Ellipsis
5✔
113

114
                # Handle Enum types with auto-generated EnumArgument
115
                if isinstance(kind, EnumMeta):
5✔
116
                    argument = EnumArgument(kind, default=argument)
5✔
117
                elif _type_is_bool(kind):
5✔
118
                    # For inherited bools, Ellipsis means use default False
119
                    if argument is Ellipsis:
5✔
120
                        argument = False
5✔
121
                    argument = _make_action_true_argument(kind, argument)
5✔
122
                else:
123
                    optional_type = unwrap_optional(kind)
5✔
124
                    if optional_type is not None:
5✔
125
                        is_required = False
5✔
126
                        kind = optional_type
5✔
127

128
                    # Handle Literal types like Literal["a", "b", "c"]
129
                    literal_info = unwrap_literal(kind)
5✔
130
                    if literal_info is not None:
5✔
131
                        value_type, choices = literal_info
5✔
132
                        argument = TypedArgument(
5✔
133
                            type=value_type,
134
                            choices=choices,
135
                            default=argument,
136
                            required=is_required,
137
                        )
138
                    # Handle container types like list[str], List[int], etc.
139
                    elif (ctr_info := _unwrap_container_type(kind)) is not None:
5✔
140
                        container_type, element_type = ctr_info
5✔
141
                        # Use nargs="+" for required, "*" for optional
142
                        if is_required:
5✔
143
                            nargs: Union[str, Nargs] = Nargs.ONE_OR_MORE
5✔
144
                        else:
145
                            nargs = Nargs.ZERO_OR_MORE
5✔
146
                        # Use converter for non-list containers
147
                        if container_type is not list:
5✔
148
                            converter = container_type
5✔
149
                        else:
150
                            converter = None
5✔
151
                        default = None if argument is Ellipsis else argument
5✔
152
                        argument = TypedArgument(
5✔
153
                            type=element_type,
154
                            default=default,
155
                            required=is_required,
156
                            nargs=nargs,
157
                            converter=converter,
158
                        )
159
                    else:
160
                        argument = TypedArgument(
5✔
161
                            type=kind,
162
                            default=argument,
163
                            required=is_required,
164
                        )
165

166
            if isinstance(argument, TypedArgument):
5✔
167
                if argument.type is None and argument.converter is None:
5✔
168
                    # First try to unwrap optional
169
                    optional_inner = unwrap_optional(kind)
5✔
170
                    if optional_inner is not None:
5✔
171
                        kind = optional_inner
5✔
172
                        if argument.default is None:
5!
173
                            argument.default = None
5✔
174

175
                    # Handle bool type: set STORE_TRUE/STORE_FALSE action
176
                    if kind is bool and argument.action == Actions.default():
5✔
177
                        default = argument.default
5✔
178
                        if default is False or default is None:
5✔
179
                            argument = argument.copy(
5✔
180
                                action=Actions.STORE_TRUE,
181
                                default=False,
182
                                type=None,
183
                            )
184
                        elif default is True:
5!
185
                            argument = argument.copy(
5✔
186
                                action=Actions.STORE_FALSE,
187
                                default=True,
188
                                type=None,
189
                            )
190
                    # Handle Literal types
191
                    elif (lit_info := unwrap_literal(kind)) is not None:
5✔
192
                        value_type, choices = lit_info
5✔
193
                        argument.type = value_type
5✔
194
                        if argument.choices is None:
5✔
195
                            argument.choices = choices
5✔
196
                    # Then check for container types
197
                    elif (
5✔
198
                        container_info := _unwrap_container_type(kind)
199
                    ) is not None:
200
                        container_type, element_type = container_info
5✔
201
                        argument.type = element_type
5✔
202
                        # Only set nargs if not already specified
203
                        if argument.nargs is None:
5!
204
                            argument.nargs = Nargs.ZERO_OR_MORE
×
205
                        # Only set converter for non-list containers
206
                        is_non_list = container_type is not list
5✔
207
                        if is_non_list and argument.converter is None:
5!
208
                            argument.converter = container_type
×
209
                    else:
210
                        argument.type = kind
5✔
211
                arguments[key] = argument
5✔
212
            elif isinstance(argument, AbstractGroup):
5✔
213
                argument_groups[key] = argument
5✔
214

215
        for key, value in attrs.items():
5✔
216
            if key.startswith("_"):
5✔
217
                continue
5✔
218

219
            # Skip if already processed from annotations
220
            if key in arguments or key in argument_groups or key in subparsers:
5✔
221
                continue
5✔
222

223
            if isinstance(value, TypedArgument):
5✔
224
                arguments[key] = value
5✔
225
            elif isinstance(value, AbstractGroup):
5✔
226
                argument_groups[key] = value
5✔
227
            elif isinstance(value, AbstractParser):
5✔
228
                subparsers[key] = value
5✔
229

230
        setattr(cls, "__arguments__", MappingProxyType(arguments))
5✔
231
        setattr(cls, "__argument_groups__", MappingProxyType(argument_groups))
5✔
232
        setattr(cls, "__subparsers__", MappingProxyType(subparsers))
5✔
233
        return cls
5✔
234

235

236
class Base(metaclass=Meta):
5✔
237
    """Base class for Parser and Group."""
238

239
    __arguments__: Mapping[str, TypedArgument]
5✔
240
    __argument_groups__: Mapping[str, "Group"]
5✔
241
    __subparsers__: Mapping[str, "Parser"]
5✔
242

243
    def __getattribute__(self, item: str) -> Any:
5✔
244
        value = super().__getattribute__(item)
5✔
245
        if item.startswith("_"):
5✔
246
            return value
5✔
247

248
        if item in self.__arguments__:
5✔
249
            class_value = getattr(self.__class__, item, None)
5✔
250
            if value is class_value:
5✔
251
                raise AttributeError(f"Attribute {item!r} was not parsed")
5✔
252
        return value
5✔
253

254
    def __repr__(self) -> str:
5✔
255
        return (
5✔
256
            f"<{self.__class__.__name__}: "
257
            f"{len(self.__arguments__)} arguments, "
258
            f"{len(self.__argument_groups__)} groups, "
259
            f"{len(self.__subparsers__)} subparsers>"
260
        )
261

262

263
class Destination(NamedTuple):
5✔
264
    """Stores destination information for parsed arguments."""
265

266
    target: Base
5✔
267
    attribute: str
5✔
268
    argument: Optional[TypedArgument]
5✔
269
    action: Optional[Action]
5✔
270

271

272
DestinationsType = MutableMapping[str, Set[Destination]]
5✔
273

274

275
class Group(AbstractGroup, Base):
5✔
276
    """Argument group for organizing related arguments."""
277

278
    def __init__(
5✔
279
        self,
280
        title: Optional[str] = None,
281
        description: Optional[str] = None,
282
        prefix: Optional[str] = None,
283
        defaults: Optional[Mapping[str, Any]] = None,
284
    ):
285
        self._title = title
5✔
286
        self._description = description
5✔
287
        self._prefix = prefix
5✔
288
        self._defaults: Mapping[str, Any] = defaults or {}
5✔
289

290

291
ParserType = TypeVar("ParserType", bound="Parser")
5✔
292

293

294
# noinspection PyProtectedMember
295
class Parser(AbstractParser, Base):
5✔
296
    """Main parser class for command-line argument parsing."""
297

298
    HELP_APPENDIX_PREAMBLE = (
5✔
299
        " Default values will based on following "
300
        "configuration files {configs}. "
301
    )
302
    HELP_APPENDIX_CURRENT = (
5✔
303
        "Now {num_existent} files has been applied {existent}. "
304
    )
305
    HELP_APPENDIX_END = (
5✔
306
        "The configuration files is INI-formatted files "
307
        "where configuration groups is INI sections. "
308
        "See more https://docs.argclass.com/config-files.html"
309
    )
310

311
    def _add_argument(
5✔
312
        self,
313
        parser: Any,
314
        argument: TypedArgument,
315
        dest: str,
316
        *aliases: str,
317
    ) -> Tuple[str, Action]:
318
        kwargs = argument.get_kwargs()
5✔
319

320
        if not argument.is_positional:
5✔
321
            kwargs["dest"] = dest
5✔
322

323
        if (
5✔
324
            argument.default is not None
325
            and argument.default is not ...
326
            and not argument.secret
327
        ):
328
            kwargs["help"] = (
5✔
329
                f"{kwargs.get('help', '')} (default: {argument.default})"
330
            ).strip()
331

332
        if argument.env_var is not None:
5✔
333
            default = kwargs.get("default")
5✔
334
            kwargs["default"] = os.getenv(argument.env_var, default)
5✔
335

336
            if kwargs["default"] and argument.is_nargs:
5✔
337
                kwargs["default"] = list(
5✔
338
                    map(
339
                        argument.type or str,
340
                        ast.literal_eval(kwargs["default"]),
341
                    ),
342
                )
343

344
            kwargs["help"] = (
5✔
345
                f"{kwargs.get('help', '')} [ENV: {argument.env_var}]"
346
            ).strip()
347

348
            if argument.env_var in os.environ:
5✔
349
                self._used_env_vars.add(argument.env_var)
5✔
350
                if argument.secret:
5✔
351
                    self._used_secret_env_vars.add(argument.env_var)
5✔
352

353
        # Convert string boolean values from config/env to proper bools
354
        action = kwargs.get("action")
5✔
355
        default = kwargs.get("default")
5✔
356
        if isinstance(default, str) and action in (
5✔
357
            Actions.STORE_TRUE,
358
            Actions.STORE_FALSE,
359
            "store_true",
360
            "store_false",
361
        ):
362
            kwargs["default"] = parse_bool(default)
5✔
363

364
        default = kwargs.get("default")
5✔
365
        if default is not None and default is not ...:
5✔
366
            kwargs["required"] = False
5✔
367

368
        return dest, parser.add_argument(*aliases, **kwargs)
5✔
369

370
    @staticmethod
5✔
371
    def get_cli_name(name: str) -> str:
5✔
372
        return name.replace("_", "-")
5✔
373

374
    def get_env_var(self, name: str, argument: TypedArgument) -> Optional[str]:
5✔
375
        if argument.env_var is not None:
5✔
376
            return argument.env_var
5✔
377
        if self._auto_env_var_prefix is not None:
5✔
378
            return f"{self._auto_env_var_prefix}{name}".upper()
5✔
379
        return None
5✔
380

381
    def __init__(
5✔
382
        self,
383
        config_files: Iterable[Union[str, Path]] = (),
384
        auto_env_var_prefix: Optional[str] = None,
385
        strict_config: bool = False,
386
        config_parser_class: Type[AbstractDefaultsParser] = INIDefaultsParser,
387
        **kwargs: Any,
388
    ):
389
        super().__init__()
5✔
390
        self.current_subparsers: Tuple[AbstractParser, ...] = ()
5✔
391
        self._config_files = config_files
5✔
392

393
        # Parse config files using the specified parser class
394
        config_parser = config_parser_class(config_files, strict=strict_config)
5✔
395
        self._config = config_parser.parse()
5✔
396
        filenames = config_parser.loaded_files
5✔
397

398
        self._epilog = kwargs.pop("epilog", "")
5✔
399

400
        if config_files:
5✔
401
            # If not config files, we don't need to add any to the epilog
402
            self._epilog += self.HELP_APPENDIX_PREAMBLE.format(
5✔
403
                configs=repr(config_files),
404
            )
405

406
            if filenames:
5✔
407
                self._epilog += self.HELP_APPENDIX_CURRENT.format(
5✔
408
                    num_existent=len(filenames),
409
                    existent=repr(list(map(str, filenames))),
410
                )
411
            self._epilog += self.HELP_APPENDIX_END
5✔
412

413
        self._auto_env_var_prefix = auto_env_var_prefix
5✔
414
        self._parser_kwargs = kwargs
5✔
415
        self._used_env_vars: Set[str] = set()
5✔
416
        self._used_secret_env_vars: Set[str] = set()
5✔
417

418
    @property
5✔
419
    def current_subparser(self) -> Optional["AbstractParser"]:
5✔
420
        if not self.current_subparsers:
5✔
421
            return None
5✔
422
        return self.current_subparsers[0]
5✔
423

424
    def _make_parser(
5✔
425
        self,
426
        parser: Optional[ArgumentParser] = None,
427
        parent_chain: Tuple["AbstractParser", ...] = (),
428
    ) -> Tuple[ArgumentParser, DestinationsType]:
429
        if parser is None:
5✔
430
            parser = ArgumentParser(
5✔
431
                epilog=self._epilog,
432
                **self._parser_kwargs,
433
            )
434

435
        destinations: DestinationsType = defaultdict(set)
5✔
436
        self._fill_arguments(destinations, parser)
5✔
437
        self._fill_groups(destinations, parser)
5✔
438
        if self.__subparsers__:
5✔
439
            self._fill_subparsers(destinations, parser, parent_chain)
5✔
440

441
        return parser, destinations
5✔
442

443
    def create_parser(self) -> ArgumentParser:
5✔
444
        """
445
        Create an ArgumentParser instance without parsing arguments.
446
        Can be used to inspect the parser structure in external integrations.
447
        NOT AN ALTERNATIVE TO parse_args, because it does not back populates
448
        the parser attributes.
449
        """
450
        parser, _ = self._make_parser()
×
451
        return parser
×
452

453
    def _fill_arguments(
5✔
454
        self,
455
        destinations: DestinationsType,
456
        parser: ArgumentParser,
457
    ) -> None:
458
        for name, argument in self.__arguments__.items():
5✔
459
            aliases = set(argument.aliases)
5✔
460

461
            # Add default alias
462
            if not aliases:
5✔
463
                aliases.add(f"--{self.get_cli_name(name)}")
5✔
464

465
            default = self._config.get(name, argument.default)
5✔
466
            argument = argument.copy(
5✔
467
                aliases=aliases,
468
                env_var=self.get_env_var(name, argument),
469
                default=default,
470
            )
471

472
            if default is not None and default is not ... and argument.required:
5✔
473
                argument = argument.copy(required=False)
5✔
474

475
            dest, action = self._add_argument(parser, argument, name, *aliases)
5✔
476
            destinations[dest].add(
5✔
477
                Destination(
478
                    target=self,
479
                    attribute=name,
480
                    argument=argument,
481
                    action=action,
482
                ),
483
            )
484

485
    def _fill_groups(
5✔
486
        self,
487
        destinations: DestinationsType,
488
        parser: ArgumentParser,
489
    ) -> None:
490
        for group_name, group in self.__argument_groups__.items():
5✔
491
            group_parser = parser.add_argument_group(
5✔
492
                title=group._title,
493
                description=group._description,
494
            )
495
            config = self._config.get(group_name, {})
5✔
496

497
            for name, argument in group.__arguments__.items():
5✔
498
                aliases = set(argument.aliases)
5✔
499
                if group._prefix is not None:
5✔
500
                    prefix = group._prefix
5✔
501
                else:
502
                    prefix = group_name
5✔
503
                dest = f"{prefix}_{name}" if prefix else name
5✔
504

505
                if not aliases:
5✔
506
                    aliases.add(f"--{self.get_cli_name(dest)}")
5✔
507

508
                default = config.get(
5✔
509
                    name,
510
                    group._defaults.get(name, argument.default),
511
                )
512
                argument = argument.copy(
5✔
513
                    default=default,
514
                    env_var=self.get_env_var(dest, argument),
515
                )
516
                dest, action = self._add_argument(
5✔
517
                    group_parser,
518
                    argument,
519
                    dest,
520
                    *aliases,
521
                )
522
                destinations[dest].add(
5✔
523
                    Destination(
524
                        target=group,
525
                        attribute=name,
526
                        argument=argument,
527
                        action=action,
528
                    ),
529
                )
530

531
    def _fill_subparsers(
5✔
532
        self,
533
        destinations: DestinationsType,
534
        parser: ArgumentParser,
535
        parent_chain: Tuple["AbstractParser", ...] = (),
536
    ) -> None:
537
        subparsers = parser.add_subparsers()
5✔
538
        subparser: AbstractParser
539
        destinations["current_subparsers"].add(
5✔
540
            Destination(
541
                target=self,
542
                attribute="current_subparsers",
543
                argument=None,
544
                action=None,
545
            ),
546
        )
547

548
        for subparser_name, subparser in self.__subparsers__.items():
5✔
549
            # Build the chain for this subparser level
550
            subparser_chain = (subparser,) + parent_chain
5✔
551
            current_parser, subparser_dests = subparser._make_parser(
5✔
552
                subparsers.add_parser(
553
                    subparser_name,
554
                    **subparser._parser_kwargs,
555
                ),
556
                parent_chain=subparser_chain,
557
            )
558
            subparser.__parent__ = self
5✔
559
            current_parser.set_defaults(
5✔
560
                current_subparsers=subparser_chain,
561
            )
562
            for key, value in subparser_dests.items():
5✔
563
                destinations[key].update(value)
5✔
564

565
    def parse_args(
5✔
566
        self: ParserType,
567
        args: Optional[List[str]] = None,
568
        sanitize_secrets: bool = False,
569
    ) -> ParserType:
570
        parser, destinations = self._make_parser()
5✔
571
        parsed_ns = parser.parse_args(args=args)
5✔
572

573
        # Get the chain of selected subparsers from the namespace
574
        selected_subparsers: Tuple[AbstractParser, ...] = getattr(
5✔
575
            parsed_ns, "current_subparsers", ()
576
        )
577

578
        for key, dests in destinations.items():
5✔
579
            for dest in dests:
5✔
580
                target = dest.target
5✔
581
                name = dest.attribute
5✔
582
                argument = dest.argument
5✔
583
                action = dest.action
5✔
584

585
                # Skip subparsers that weren't selected
586
                if (
5✔
587
                    isinstance(target, AbstractParser)
588
                    and target is not self
589
                    and target not in selected_subparsers
590
                ):
591
                    continue
5✔
592

593
                parsed_value = getattr(parsed_ns, key, None)
5✔
594

595
                if isinstance(action, ConfigAction):
5✔
596
                    action(parser, parsed_ns, parsed_value, None)
5✔
597
                    parsed_value = getattr(parsed_ns, key)
5✔
598

599
                if argument is not None:
5✔
600
                    if argument.secret and isinstance(parsed_value, str):
5✔
601
                        parsed_value = SecretString(parsed_value)
5✔
602
                    if argument.converter is not None:
5✔
603
                        if argument.nargs and parsed_value is None:
5✔
604
                            parsed_value = []
5✔
605
                        try:
5✔
606
                            parsed_value = argument.converter(parsed_value)
5✔
607
                        except Exception as e:
5✔
608
                            msg = f"failed to convert {parsed_value!r}: {e}"
5✔
609
                            raise ArgumentError(action, msg) from e
5✔
610

611
                # Ensure current_subparsers is always a tuple, not None
612
                if name == "current_subparsers" and parsed_value is None:
5✔
613
                    parsed_value = ()
5✔
614

615
                setattr(target, name, parsed_value)
5✔
616

617
        if sanitize_secrets:
5✔
618
            for name in self._used_secret_env_vars:
5✔
619
                os.environ.pop(name, None)
5✔
620
            self._used_secret_env_vars.clear()
5✔
621

622
        return self
5✔
623

624
    def print_help(self) -> None:
5✔
625
        parser, _ = self._make_parser()
5✔
626
        return parser.print_help()
5✔
627

628
    def sanitize_env(self, only_secrets: bool = False) -> None:
5✔
629
        if only_secrets:
5✔
630
            for name in self._used_secret_env_vars:
5✔
631
                os.environ.pop(name, None)
5✔
632
            self._used_secret_env_vars.clear()
5✔
633
        else:
634
            for name in self._used_env_vars:
5✔
635
                os.environ.pop(name, None)
5✔
636
            self._used_env_vars.clear()
5✔
637
            self._used_secret_env_vars.clear()
5✔
638

639
    def __call__(self) -> Any:
5✔
640
        """
641
        Override this function if you want to equip your parser with an action.
642

643
        By default this calls the current_subparser's __call__ method if
644
        there is a current_subparser, otherwise returns None.
645

646
        Example:
647
            class Parser(argclass.Parser):
648
                def __call__(self) -> Any:
649
                    print("Hello world!")
650

651
            parser = Parser()
652
            parser.parse_args([])
653
            parser()  # Will print "Hello world!"
654

655
        When you have subparsers:
656
            class SubParser(argclass.Parser):
657
                def __call__(self) -> Any:
658
                    print("In subparser!")
659

660
            class Parser(argclass.Parser):
661
                sub = SubParser()
662

663
            parser = Parser()
664
            parser.parse_args(["sub"])
665
            parser()  # Will print "In subparser!"
666
        """
667
        if self.current_subparser is not None:
5!
668
            return self.current_subparser()
5✔
UNCOV
669
        return None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc