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

mosquito / argclass / 20790208622

07 Jan 2026 05:23PM UTC coverage: 99.507% (+7.6%) from 91.914%
20790208622

Pull #35

github

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

269 of 270 branches covered (99.63%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 2 files covered. (100.0%)

4 existing lines in 2 files now uncovered.

740 of 744 relevant lines covered (99.46%)

4.96 hits per line

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

99.57
/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")
5✔
57
    else:  # kind == Optional[bool], only other case from _type_is_bool
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
                        else:
191
                            raise TypeError(
5✔
192
                                f"Invalid default {default!r} for bool"
193
                            )
194
                    # Handle Literal types
195
                    elif (lit_info := unwrap_literal(kind)) is not None:
5✔
196
                        value_type, choices = lit_info
5✔
197
                        argument.type = value_type
5✔
198
                        if argument.choices is None:
5✔
199
                            argument.choices = choices
5✔
200
                    # Then check for container types
201
                    elif (
5✔
202
                        container_info := _unwrap_container_type(kind)
203
                    ) is not None:
204
                        container_type, element_type = container_info
5✔
205
                        argument.type = element_type
5✔
206
                        # Only set nargs if not already specified
207
                        if argument.nargs is None:
5✔
208
                            argument.nargs = Nargs.ZERO_OR_MORE
5✔
209
                        # Only set converter for non-list containers
210
                        is_non_list = container_type is not list
5✔
211
                        if is_non_list and argument.converter is None:
5✔
212
                            argument.converter = container_type
5✔
213
                    else:
214
                        argument.type = kind
5✔
215
                arguments[key] = argument
5✔
216
            elif isinstance(argument, AbstractGroup):
5✔
217
                argument_groups[key] = argument
5✔
218

219
        for key, value in attrs.items():
5✔
220
            if key.startswith("_"):
5✔
221
                continue
5✔
222

223
            # Skip if already processed from annotations
224
            if key in arguments or key in argument_groups or key in subparsers:
5✔
225
                continue
5✔
226

227
            if isinstance(value, TypedArgument):
5✔
228
                arguments[key] = value
5✔
229
            elif isinstance(value, AbstractGroup):
5✔
230
                argument_groups[key] = value
5✔
231
            elif isinstance(value, AbstractParser):
5✔
232
                subparsers[key] = value
5✔
233

234
        setattr(cls, "__arguments__", MappingProxyType(arguments))
5✔
235
        setattr(cls, "__argument_groups__", MappingProxyType(argument_groups))
5✔
236
        setattr(cls, "__subparsers__", MappingProxyType(subparsers))
5✔
237
        return cls
5✔
238

239

240
class Base(metaclass=Meta):
5✔
241
    """Base class for Parser and Group."""
242

243
    __arguments__: Mapping[str, TypedArgument]
4✔
244
    __argument_groups__: Mapping[str, "Group"]
4✔
245
    __subparsers__: Mapping[str, "Parser"]
4✔
246

247
    def __getattribute__(self, item: str) -> Any:
5✔
248
        value = super().__getattribute__(item)
5✔
249
        if item.startswith("_"):
5✔
250
            return value
5✔
251

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

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

266

267
class Destination(NamedTuple):
5✔
268
    """Stores destination information for parsed arguments."""
269

270
    target: Base
4✔
271
    attribute: str
4✔
272
    argument: Optional[TypedArgument]
4✔
273
    action: Optional[Action]
4✔
274

275

276
DestinationsType = MutableMapping[str, Set[Destination]]
5✔
277

278

279
class Group(AbstractGroup, Base):
5✔
280
    """Argument group for organizing related arguments."""
281

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

294

295
ParserType = TypeVar("ParserType", bound="Parser")
5✔
296

297

298
# noinspection PyProtectedMember
299
class Parser(AbstractParser, Base):
5✔
300
    """Main parser class for command-line argument parsing."""
301

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

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

324
        if not argument.is_positional:
5✔
325
            kwargs["dest"] = dest
5✔
326

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

336
        if argument.env_var is not None:
5✔
337
            default = kwargs.get("default")
5✔
338
            kwargs["default"] = os.getenv(argument.env_var, default)
5✔
339

340
            if kwargs["default"] and argument.is_nargs:
5✔
341
                kwargs["default"] = list(
5✔
342
                    map(
343
                        argument.type or str,
344
                        ast.literal_eval(kwargs["default"]),
345
                    ),
346
                )
347

348
            kwargs["help"] = (
5✔
349
                f"{kwargs.get('help', '')} [ENV: {argument.env_var}]"
350
            ).strip()
351

352
            if argument.env_var in os.environ:
5✔
353
                self._used_env_vars.add(argument.env_var)
5✔
354
                if argument.secret:
5✔
355
                    self._used_secret_env_vars.add(argument.env_var)
5✔
356

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

368
        default = kwargs.get("default")
5✔
369
        if default is not None and default is not ...:
5✔
370
            kwargs["required"] = False
5✔
371

372
        return dest, parser.add_argument(*aliases, **kwargs)
5✔
373

374
    @staticmethod
5✔
375
    def get_cli_name(name: str) -> str:
5✔
376
        return name.replace("_", "-")
5✔
377

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

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

397
        # Parse config files using the specified parser class
398
        config_parser = config_parser_class(config_files, strict=strict_config)
5✔
399
        self._config = config_parser.parse()
5✔
400
        filenames = config_parser.loaded_files
5✔
401

402
        self._epilog = kwargs.pop("epilog", "")
5✔
403

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

410
            if filenames:
5✔
411
                self._epilog += self.HELP_APPENDIX_CURRENT.format(
5✔
412
                    num_existent=len(filenames),
413
                    existent=repr(list(map(str, filenames))),
414
                )
415
            self._epilog += self.HELP_APPENDIX_END
5✔
416

417
        self._auto_env_var_prefix = auto_env_var_prefix
5✔
418
        self._parser_kwargs = kwargs
5✔
419
        self._used_env_vars: Set[str] = set()
5✔
420
        self._used_secret_env_vars: Set[str] = set()
5✔
421

422
    @property
5✔
423
    def current_subparser(self) -> Optional["AbstractParser"]:
5✔
424
        if not self.current_subparsers:
5✔
425
            return None
5✔
426
        return self.current_subparsers[0]
5✔
427

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

439
        destinations: DestinationsType = defaultdict(set)
5✔
440
        self._fill_arguments(destinations, parser)
5✔
441
        self._fill_groups(destinations, parser)
5✔
442
        if self.__subparsers__:
5✔
443
            self._fill_subparsers(destinations, parser, parent_chain)
5✔
444

445
        return parser, destinations
5✔
446

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

457
    def _fill_arguments(
5✔
458
        self,
459
        destinations: DestinationsType,
460
        parser: ArgumentParser,
461
    ) -> None:
462
        for name, argument in self.__arguments__.items():
5✔
463
            aliases = set(argument.aliases)
5✔
464

465
            # Add default alias
466
            if not aliases:
5✔
467
                aliases.add(f"--{self.get_cli_name(name)}")
5✔
468

469
            default = self._config.get(name, argument.default)
5✔
470
            argument = argument.copy(
5✔
471
                aliases=aliases,
472
                env_var=self.get_env_var(name, argument),
473
                default=default,
474
            )
475

476
            if default is not None and default is not ... and argument.required:
5✔
477
                argument = argument.copy(required=False)
5✔
478

479
            dest, action = self._add_argument(parser, argument, name, *aliases)
5✔
480
            destinations[dest].add(
5✔
481
                Destination(
482
                    target=self,
483
                    attribute=name,
484
                    argument=argument,
485
                    action=action,
486
                ),
487
            )
488

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

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

509
                if not aliases:
5✔
510
                    aliases.add(f"--{self.get_cli_name(dest)}")
5✔
511

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

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

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

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

577
        # Get the chain of selected subparsers from the namespace
578
        selected_subparsers: Tuple[AbstractParser, ...] = getattr(
5✔
579
            parsed_ns, "current_subparsers", ()
580
        )
581

582
        for key, dests in destinations.items():
5✔
583
            for dest in dests:
5✔
584
                target = dest.target
5✔
585
                name = dest.attribute
5✔
586
                argument = dest.argument
5✔
587
                action = dest.action
5✔
588

589
                # Skip subparsers that weren't selected
590
                if (
5✔
591
                    isinstance(target, AbstractParser)
592
                    and target is not self
593
                    and target not in selected_subparsers
594
                ):
595
                    continue
5✔
596

597
                parsed_value = getattr(parsed_ns, key, None)
5✔
598

599
                if isinstance(action, ConfigAction):
5✔
600
                    action(parser, parsed_ns, parsed_value, None)
5✔
601
                    parsed_value = getattr(parsed_ns, key)
5✔
602

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

615
                # Ensure current_subparsers is always a tuple, not None
616
                if name == "current_subparsers" and parsed_value is None:
5✔
617
                    parsed_value = ()
5✔
618

619
                setattr(target, name, parsed_value)
5✔
620

621
        if sanitize_secrets:
5✔
622
            for name in self._used_secret_env_vars:
5✔
623
                os.environ.pop(name, None)
5✔
624
            self._used_secret_env_vars.clear()
5✔
625

626
        return self
5✔
627

628
    def print_help(self) -> None:
5✔
629
        parser, _ = self._make_parser()
5✔
630
        return parser.print_help()
5✔
631

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

643
    def __call__(self) -> Any:
5✔
644
        """
645
        Override this function if you want to equip your parser with an action.
646

647
        By default this calls the current_subparser's __call__ method if
648
        there is a current_subparser, otherwise returns None.
649

650
        Example:
651
            class Parser(argclass.Parser):
652
                def __call__(self) -> Any:
653
                    print("Hello world!")
654

655
            parser = Parser()
656
            parser.parse_args([])
657
            parser()  # Will print "Hello world!"
658

659
        When you have subparsers:
660
            class SubParser(argclass.Parser):
661
                def __call__(self) -> Any:
662
                    print("In subparser!")
663

664
            class Parser(argclass.Parser):
665
                sub = SubParser()
666

667
            parser = Parser()
668
            parser.parse_args(["sub"])
669
            parser()  # Will print "In subparser!"
670
        """
671
        if self.current_subparser is not None:
5✔
672
            return self.current_subparser()
5✔
673
        return None
5✔
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