#!/usr/bin/env python
# cardinal_pythonlib/argparse_func.py
"""
===============================================================================
Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).
This file is part of cardinal_pythonlib.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
**Functions to help with argparse.**
"""
# noinspection PyProtectedMember
from argparse import (
_HelpAction,
_SubParsersAction,
ArgumentDefaultsHelpFormatter,
ArgumentParser,
ArgumentTypeError,
Namespace,
RawDescriptionHelpFormatter,
)
from typing import Any, Dict, List, Type
from rich_argparse import (
ArgumentDefaultsRichHelpFormatter,
RawDescriptionRichHelpFormatter,
)
# =============================================================================
# Argparse actions
# =============================================================================
[docs]class ShowAllSubparserHelpAction(_HelpAction):
"""
Class to serve as the ``action`` for an ``argparse`` top-level parser that
shows help for all subparsers. As per
https://stackoverflow.com/questions/20094215/argparse-subparser-monolithic-help-output
"""
def __call__(
self,
parser: ArgumentParser,
namespace: Namespace,
values: List[Any], # ?
option_string: str = None,
) -> None:
# 1. Print top-level help
parser.print_help()
sep = "=" * 79 # "-" less helpful when using grep for "--option"!
# 2. Print help for all subparsers
# noinspection PyProtectedMember
subparsers_actions = [
action
for action in parser._actions
if isinstance(action, _SubParsersAction)
] # type: List[_SubParsersAction]
messages = [""] # type: List[str]
for subparsers_action in subparsers_actions:
for choice, subparser in subparsers_action.choices.items():
messages.append(sep)
messages.append(f"Help for command '{choice}'")
messages.append(sep)
messages.append(subparser.format_help())
print("\n".join(messages))
parser.exit()
# =============================================================================
# Argparse formatters
# =============================================================================
# =============================================================================
# Argparse types/checkers
# =============================================================================
[docs]def str2bool(v: str) -> bool:
"""
``argparse`` type that maps strings in case-insensitive fashion like this:
.. code-block:: none
argument strings value
------------------------------- -----
'yes', 'true', 't', 'y', '1' True
'no', 'false', 'f', 'n', '0' False
From
https://stackoverflow.com/questions/15008758/parsing-boolean-values-with-argparse
Specimen usage:
.. code-block:: python
parser.add_argument(
"--nice", type=str2bool, nargs='?',
const=True, # if --nice is present with no parameter
default=NICE, # if the argument is entirely absent
help="Activate nice mode.")
"""
lv = v.lower()
if lv in ("yes", "true", "t", "y", "1"):
return True
elif lv in ("no", "false", "f", "n", "0"):
return False
else:
raise ArgumentTypeError("Boolean value expected.")
[docs]def positive_int(value: str) -> int:
"""
``argparse`` argument type that checks that its value is a positive
integer.
"""
try:
ivalue = int(value)
assert ivalue > 0
except (AssertionError, TypeError, ValueError):
raise ArgumentTypeError(f"{value!r} is an invalid positive int")
return ivalue
[docs]def nonnegative_int(value: str) -> int:
"""
``argparse`` argument type that checks that its value is a non-negative
integer.
"""
try:
ivalue = int(value)
assert ivalue >= 0
except (AssertionError, TypeError, ValueError):
raise ArgumentTypeError(f"{value!r} is an invalid non-negative int")
return ivalue
[docs]def percentage(value: str) -> float:
"""
``argparse`` argument type that checks that its value is a percentage (in
the sense of a float in the range [0, 100]).
"""
try:
fvalue = float(value)
assert 0 <= fvalue <= 100
except (AssertionError, TypeError, ValueError):
raise ArgumentTypeError(f"{value!r} is an invalid percentage value")
return fvalue
[docs]class MapType(object):
"""
``argparse`` type maker that maps strings to a dictionary (map).
"""
def __init__(
self,
map_separator: str = ":",
pair_separator: str = ",",
strip: bool = True,
from_type: Type = str,
to_type: Type = str,
) -> None:
"""
Args:
map_separator:
string that separates the "from" and "to" members of a pair
pair_separator:
string that separates different pairs
strip:
strip whitespace after splitting?
from_type:
type to coerce "from" values to; e.g. ``str``, ``int``
to_type:
type to coerce "to" values to; e.g. ``str``, ``int``
"""
self.map_separator = map_separator
self.pair_separator = pair_separator
self.strip = strip
self.from_type = from_type
self.to_type = to_type
def __call__(self, value: str) -> Dict:
result = {}
pairs = value.split(self.pair_separator)
for pair in pairs:
from_str, to_str = pair.split(self.map_separator)
if self.strip:
from_str = from_str.strip()
to_str = to_str.strip()
try:
from_val = self.from_type(from_str)
except (TypeError, ValueError):
raise ArgumentTypeError(
f"{from_str!r} cannot be converted to type "
f"{self.from_type!r}"
)
try:
to_val = self.to_type(to_str)
except (TypeError, ValueError):
raise ArgumentTypeError(
f"{to_str!r} cannot be converted to type "
f"{self.to_type!r}"
)
result[from_val] = to_val
return result