Files
api/path/to/venv/lib/python3.12/site-packages/invoke/parser/context.py
2025-12-30 11:27:14 +07:00

267 lines
9.6 KiB
Python

import itertools
from typing import Any, Dict, List, Iterable, Optional, Tuple, Union
try:
from ..vendor.lexicon import Lexicon
except ImportError:
from lexicon import Lexicon # type: ignore[no-redef]
from .argument import Argument
def translate_underscores(name: str) -> str:
return name.lstrip("_").rstrip("_").replace("_", "-")
def to_flag(name: str) -> str:
name = translate_underscores(name)
if len(name) == 1:
return "-" + name
return "--" + name
def sort_candidate(arg: Argument) -> str:
names = arg.names
# TODO: is there no "split into two buckets on predicate" builtin?
shorts = {x for x in names if len(x.strip("-")) == 1}
longs = {x for x in names if x not in shorts}
return str(sorted(shorts if shorts else longs)[0])
def flag_key(arg: Argument) -> List[Union[int, str]]:
"""
Obtain useful key list-of-ints for sorting CLI flags.
.. versionadded:: 1.0
"""
# Setup
ret: List[Union[int, str]] = []
x = sort_candidate(arg)
# Long-style flags win over short-style ones, so the first item of
# comparison is simply whether the flag is a single character long (with
# non-length-1 flags coming "first" [lower number])
ret.append(1 if len(x) == 1 else 0)
# Next item of comparison is simply the strings themselves,
# case-insensitive. They will compare alphabetically if compared at this
# stage.
ret.append(x.lower())
# Finally, if the case-insensitive test also matched, compare
# case-sensitive, but inverse (with lowercase letters coming first)
inversed = ""
for char in x:
inversed += char.lower() if char.isupper() else char.upper()
ret.append(inversed)
return ret
# Named slightly more verbose so Sphinx references can be unambiguous.
# Got real sick of fully qualified paths.
class ParserContext:
"""
Parsing context with knowledge of flags & their format.
Generally associated with the core program or a task.
When run through a parser, will also hold runtime values filled in by the
parser.
.. versionadded:: 1.0
"""
def __init__(
self,
name: Optional[str] = None,
aliases: Iterable[str] = (),
args: Iterable[Argument] = (),
) -> None:
"""
Create a new ``ParserContext`` named ``name``, with ``aliases``.
``name`` is optional, and should be a string if given. It's used to
tell ParserContext objects apart, and for use in a Parser when
determining what chunk of input might belong to a given ParserContext.
``aliases`` is also optional and should be an iterable containing
strings. Parsing will honor any aliases when trying to "find" a given
context in its input.
May give one or more ``args``, which is a quick alternative to calling
``for arg in args: self.add_arg(arg)`` after initialization.
"""
self.args = Lexicon()
self.positional_args: List[Argument] = []
self.flags = Lexicon()
self.inverse_flags: Dict[str, str] = {} # No need for Lexicon here
self.name = name
self.aliases = aliases
for arg in args:
self.add_arg(arg)
def __repr__(self) -> str:
aliases = ""
if self.aliases:
aliases = " ({})".format(", ".join(self.aliases))
name = (" {!r}{}".format(self.name, aliases)) if self.name else ""
args = (": {!r}".format(self.args)) if self.args else ""
return "<parser/Context{}{}>".format(name, args)
def add_arg(self, *args: Any, **kwargs: Any) -> None:
"""
Adds given ``Argument`` (or constructor args for one) to this context.
The Argument in question is added to the following dict attributes:
* ``args``: "normal" access, i.e. the given names are directly exposed
as keys.
* ``flags``: "flaglike" access, i.e. the given names are translated
into CLI flags, e.g. ``"foo"`` is accessible via ``flags['--foo']``.
* ``inverse_flags``: similar to ``flags`` but containing only the
"inverse" versions of boolean flags which default to True. This
allows the parser to track e.g. ``--no-myflag`` and turn it into a
False value for the ``myflag`` Argument.
.. versionadded:: 1.0
"""
# Normalize
if len(args) == 1 and isinstance(args[0], Argument):
arg = args[0]
else:
arg = Argument(*args, **kwargs)
# Uniqueness constraint: no name collisions
for name in arg.names:
if name in self.args:
msg = "Tried to add an argument named {!r} but one already exists!" # noqa
raise ValueError(msg.format(name))
# First name used as "main" name for purposes of aliasing
main = arg.names[0] # NOT arg.name
self.args[main] = arg
# Note positionals in distinct, ordered list attribute
if arg.positional:
self.positional_args.append(arg)
# Add names & nicknames to flags, args
self.flags[to_flag(main)] = arg
for name in arg.nicknames:
self.args.alias(name, to=main)
self.flags.alias(to_flag(name), to=to_flag(main))
# Add attr_name to args, but not flags
if arg.attr_name:
self.args.alias(arg.attr_name, to=main)
# Add to inverse_flags if required
if arg.kind == bool and arg.default is True:
# Invert the 'main' flag name here, which will be a dashed version
# of the primary argument name if underscore-to-dash transformation
# occurred.
inverse_name = to_flag("no-{}".format(main))
self.inverse_flags[inverse_name] = to_flag(main)
@property
def missing_positional_args(self) -> List[Argument]:
return [x for x in self.positional_args if x.value is None]
@property
def as_kwargs(self) -> Dict[str, Any]:
"""
This context's arguments' values keyed by their ``.name`` attribute.
Results in a dict suitable for use in Python contexts, where e.g. an
arg named ``foo-bar`` becomes accessible as ``foo_bar``.
.. versionadded:: 1.0
"""
ret = {}
for arg in self.args.values():
ret[arg.name] = arg.value
return ret
def names_for(self, flag: str) -> List[str]:
# TODO: should probably be a method on Lexicon/AliasDict
return list(set([flag] + self.flags.aliases_of(flag)))
def help_for(self, flag: str) -> Tuple[str, str]:
"""
Return 2-tuple of ``(flag-spec, help-string)`` for given ``flag``.
.. versionadded:: 1.0
"""
# Obtain arg obj
if flag not in self.flags:
err = "{!r} is not a valid flag for this context! Valid flags are: {!r}" # noqa
raise ValueError(err.format(flag, self.flags.keys()))
arg = self.flags[flag]
# Determine expected value type, if any
value = {str: "STRING", int: "INT"}.get(arg.kind)
# Format & go
full_names = []
for name in self.names_for(flag):
if value:
# Short flags are -f VAL, long are --foo=VAL
# When optional, also, -f [VAL] and --foo[=VAL]
if len(name.strip("-")) == 1:
value_ = ("[{}]".format(value)) if arg.optional else value
valuestr = " {}".format(value_)
else:
valuestr = "={}".format(value)
if arg.optional:
valuestr = "[{}]".format(valuestr)
else:
# no value => boolean
# check for inverse
if name in self.inverse_flags.values():
name = "--[no-]{}".format(name[2:])
valuestr = ""
# Tack together
full_names.append(name + valuestr)
namestr = ", ".join(sorted(full_names, key=len))
helpstr = arg.help or ""
return namestr, helpstr
def help_tuples(self) -> List[Tuple[str, Optional[str]]]:
"""
Return sorted iterable of help tuples for all member Arguments.
Sorts like so:
* General sort is alphanumerically
* Short flags win over long flags
* Arguments with *only* long flags and *no* short flags will come
first.
* When an Argument has multiple long or short flags, it will sort using
the most favorable (lowest alphabetically) candidate.
This will result in a help list like so::
--alpha, --zeta # 'alpha' wins
--beta
-a, --query # short flag wins
-b, --argh
-c
.. versionadded:: 1.0
"""
# TODO: argument/flag API must change :(
# having to call to_flag on 1st name of an Argument is just dumb.
# To pass in an Argument object to help_for may require moderate
# changes?
return list(
map(
lambda x: self.help_for(to_flag(x.name)),
sorted(self.flags.values(), key=flag_key),
)
)
def flag_names(self) -> Tuple[str, ...]:
"""
Similar to `help_tuples` but returns flag names only, no helpstrs.
Specifically, all flag names, flattened, in rough order.
.. versionadded:: 1.0
"""
# Regular flag names
flags = sorted(self.flags.values(), key=flag_key)
names = [self.names_for(to_flag(x.name)) for x in flags]
# Inverse flag names sold separately
names.append(list(self.inverse_flags.keys()))
return tuple(itertools.chain.from_iterable(names))