Initial commit (Clean history)

This commit is contained in:
anhduy-tech
2025-12-30 11:27:14 +07:00
commit ef48c93de0
19255 changed files with 3248867 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# flake8: noqa
from .parser import *
from .context import ParserContext
from .context import ParserContext as Context, to_flag, translate_underscores
from .argument import Argument

View File

@@ -0,0 +1,178 @@
from typing import Any, Iterable, Optional, Tuple
# TODO: dynamic type for kind
# T = TypeVar('T')
class Argument:
"""
A command-line argument/flag.
:param name:
Syntactic sugar for ``names=[<name>]``. Giving both ``name`` and
``names`` is invalid.
:param names:
List of valid identifiers for this argument. For example, a "help"
argument may be defined with a name list of ``['-h', '--help']``.
:param kind:
Type factory & parser hint. E.g. ``int`` will turn the default text
value parsed, into a Python integer; and ``bool`` will tell the
parser not to expect an actual value but to treat the argument as a
toggle/flag.
:param default:
Default value made available to the parser if no value is given on the
command line.
:param help:
Help text, intended for use with ``--help``.
:param positional:
Whether or not this argument's value may be given positionally. When
``False`` (default) arguments must be explicitly named.
:param optional:
Whether or not this (non-``bool``) argument requires a value.
:param incrementable:
Whether or not this (``int``) argument is to be incremented instead of
overwritten/assigned to.
:param attr_name:
A Python identifier/attribute friendly name, typically filled in with
the underscored version when ``name``/``names`` contain dashes.
.. versionadded:: 1.0
"""
def __init__(
self,
name: Optional[str] = None,
names: Iterable[str] = (),
kind: Any = str,
default: Optional[Any] = None,
help: Optional[str] = None,
positional: bool = False,
optional: bool = False,
incrementable: bool = False,
attr_name: Optional[str] = None,
) -> None:
if name and names:
raise TypeError(
"Cannot give both 'name' and 'names' arguments! Pick one."
)
if not (name or names):
raise TypeError("An Argument must have at least one name.")
if names:
self.names = tuple(names)
elif name and not names:
self.names = (name,)
self.kind = kind
initial_value: Optional[Any] = None
# Special case: list-type args start out as empty list, not None.
if kind is list:
initial_value = []
# Another: incrementable args start out as their default value.
if incrementable:
initial_value = default
self.raw_value = self._value = initial_value
self.default = default
self.help = help
self.positional = positional
self.optional = optional
self.incrementable = incrementable
self.attr_name = attr_name
def __repr__(self) -> str:
nicks = ""
if self.nicknames:
nicks = " ({})".format(", ".join(self.nicknames))
flags = ""
if self.positional or self.optional:
flags = " "
if self.positional:
flags += "*"
if self.optional:
flags += "?"
# TODO: store this default value somewhere other than signature of
# Argument.__init__?
kind = ""
if self.kind != str:
kind = " [{}]".format(self.kind.__name__)
return "<{}: {}{}{}{}>".format(
self.__class__.__name__, self.name, nicks, kind, flags
)
@property
def name(self) -> Optional[str]:
"""
The canonical attribute-friendly name for this argument.
Will be ``attr_name`` (if given to constructor) or the first name in
``names`` otherwise.
.. versionadded:: 1.0
"""
return self.attr_name or self.names[0]
@property
def nicknames(self) -> Tuple[str, ...]:
return self.names[1:]
@property
def takes_value(self) -> bool:
if self.kind is bool:
return False
if self.incrementable:
return False
return True
@property
def value(self) -> Any:
# TODO: should probably be optional instead
return self._value if self._value is not None else self.default
@value.setter
def value(self, arg: str) -> None:
self.set_value(arg, cast=True)
def set_value(self, value: Any, cast: bool = True) -> None:
"""
Actual explicit value-setting API call.
Sets ``self.raw_value`` to ``value`` directly.
Sets ``self.value`` to ``self.kind(value)``, unless:
- ``cast=False``, in which case the raw value is also used.
- ``self.kind==list``, in which case the value is appended to
``self.value`` instead of cast & overwritten.
- ``self.incrementable==True``, in which case the value is ignored and
the current (assumed int) value is simply incremented.
.. versionadded:: 1.0
"""
self.raw_value = value
# Default to do-nothing/identity function
func = lambda x: x
# If cast, set to self.kind, which should be str/int/etc
if cast:
func = self.kind
# If self.kind is a list, append instead of using cast func.
if self.kind is list:
func = lambda x: self.value + [x]
# If incrementable, just increment.
if self.incrementable:
# TODO: explode nicely if self.value was not an int to start
# with
func = lambda x: self.value + 1
self._value = func(value)
@property
def got_value(self) -> bool:
"""
Returns whether the argument was ever given a (non-default) value.
For most argument kinds, this simply checks whether the internally
stored value is non-``None``; for others, such as ``list`` kinds,
different checks may be used.
.. versionadded:: 1.3
"""
if self.kind is list:
return bool(self._value)
return self._value is not None

View File

@@ -0,0 +1,266 @@
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))

View File

@@ -0,0 +1,455 @@
import copy
from typing import TYPE_CHECKING, Any, Iterable, List, Optional
try:
from ..vendor.lexicon import Lexicon
from ..vendor.fluidity import StateMachine, state, transition
except ImportError:
from lexicon import Lexicon # type: ignore[no-redef]
from fluidity import ( # type: ignore[no-redef]
StateMachine,
state,
transition,
)
from ..exceptions import ParseError
from ..util import debug
if TYPE_CHECKING:
from .context import ParserContext
def is_flag(value: str) -> bool:
return value.startswith("-")
def is_long_flag(value: str) -> bool:
return value.startswith("--")
class ParseResult(List["ParserContext"]):
"""
List-like object with some extra parse-related attributes.
Specifically, a ``.remainder`` attribute, which is the string found after a
``--`` in any parsed argv list; and an ``.unparsed`` attribute, a list of
tokens that were unable to be parsed.
.. versionadded:: 1.0
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.remainder = ""
self.unparsed: List[str] = []
class Parser:
"""
Create parser conscious of ``contexts`` and optional ``initial`` context.
``contexts`` should be an iterable of ``Context`` instances which will be
searched when new context names are encountered during a parse. These
Contexts determine what flags may follow them, as well as whether given
flags take values.
``initial`` is optional and will be used to determine validity of "core"
options/flags at the start of the parse run, if any are encountered.
``ignore_unknown`` determines what to do when contexts are found which do
not map to any members of ``contexts``. By default it is ``False``, meaning
any unknown contexts result in a parse error exception. If ``True``,
encountering an unknown context halts parsing and populates the return
value's ``.unparsed`` attribute with the remaining parse tokens.
.. versionadded:: 1.0
"""
def __init__(
self,
contexts: Iterable["ParserContext"] = (),
initial: Optional["ParserContext"] = None,
ignore_unknown: bool = False,
) -> None:
self.initial = initial
self.contexts = Lexicon()
self.ignore_unknown = ignore_unknown
for context in contexts:
debug("Adding {}".format(context))
if not context.name:
raise ValueError("Non-initial contexts must have names.")
exists = "A context named/aliased {!r} is already in this parser!"
if context.name in self.contexts:
raise ValueError(exists.format(context.name))
self.contexts[context.name] = context
for alias in context.aliases:
if alias in self.contexts:
raise ValueError(exists.format(alias))
self.contexts.alias(alias, to=context.name)
def parse_argv(self, argv: List[str]) -> ParseResult:
"""
Parse an argv-style token list ``argv``.
Returns a list (actually a subclass, `.ParseResult`) of
`.ParserContext` objects matching the order they were found in the
``argv`` and containing `.Argument` objects with updated values based
on any flags given.
Assumes any program name has already been stripped out. Good::
Parser(...).parse_argv(['--core-opt', 'task', '--task-opt'])
Bad::
Parser(...).parse_argv(['invoke', '--core-opt', ...])
:param argv: List of argument string tokens.
:returns:
A `.ParseResult` (a ``list`` subclass containing some number of
`.ParserContext` objects).
.. versionadded:: 1.0
"""
machine = ParseMachine(
# FIXME: initial should not be none
initial=self.initial, # type: ignore[arg-type]
contexts=self.contexts,
ignore_unknown=self.ignore_unknown,
)
# FIXME: Why isn't there str.partition for lists? There must be a
# better way to do this. Split argv around the double-dash remainder
# sentinel.
debug("Starting argv: {!r}".format(argv))
try:
ddash = argv.index("--")
except ValueError:
ddash = len(argv) # No remainder == body gets all
body = argv[:ddash]
remainder = argv[ddash:][1:] # [1:] to strip off remainder itself
if remainder:
debug(
"Remainder: argv[{!r}:][1:] => {!r}".format(ddash, remainder)
)
for index, token in enumerate(body):
# Handle non-space-delimited forms, if not currently expecting a
# flag value and still in valid parsing territory (i.e. not in
# "unknown" state which implies store-only)
# NOTE: we do this in a few steps so we can
# split-then-check-validity; necessary for things like when the
# previously seen flag optionally takes a value.
mutations = []
orig = token
if is_flag(token) and not machine.result.unparsed:
# Equals-sign-delimited flags, eg --foo=bar or -f=bar
if "=" in token:
token, _, value = token.partition("=")
msg = "Splitting x=y expr {!r} into tokens {!r} and {!r}"
debug(msg.format(orig, token, value))
mutations.append((index + 1, value))
# Contiguous boolean short flags, e.g. -qv
elif not is_long_flag(token) and len(token) > 2:
full_token = token[:]
rest, token = token[2:], token[:2]
err = "Splitting {!r} into token {!r} and rest {!r}"
debug(err.format(full_token, token, rest))
# Handle boolean flag block vs short-flag + value. Make
# sure not to test the token as a context flag if we've
# passed into 'storing unknown stuff' territory (e.g. on a
# core-args pass, handling what are going to be task args)
have_flag = (
token in machine.context.flags
and machine.current_state != "unknown"
)
if have_flag and machine.context.flags[token].takes_value:
msg = "{!r} is a flag for current context & it takes a value, giving it {!r}" # noqa
debug(msg.format(token, rest))
mutations.append((index + 1, rest))
else:
_rest = ["-{}".format(x) for x in rest]
msg = "Splitting multi-flag glob {!r} into {!r} and {!r}" # noqa
debug(msg.format(orig, token, _rest))
for item in reversed(_rest):
mutations.append((index + 1, item))
# Here, we've got some possible mutations queued up, and 'token'
# may have been overwritten as well. Whether we apply those and
# continue as-is, or roll it back, depends:
# - If the parser wasn't waiting for a flag value, we're already on
# the right track, so apply mutations and move along to the
# handle() step.
# - If we ARE waiting for a value, and the flag expecting it ALWAYS
# wants a value (it's not optional), we go back to using the
# original token. (TODO: could reorganize this to avoid the
# sub-parsing in this case, but optimizing for human-facing
# execution isn't critical.)
# - Finally, if we are waiting for a value AND it's optional, we
# inspect the first sub-token/mutation to see if it would otherwise
# have been a valid flag, and let that determine what we do (if
# valid, we apply the mutations; if invalid, we reinstate the
# original token.)
if machine.waiting_for_flag_value:
optional = machine.flag and machine.flag.optional
subtoken_is_valid_flag = token in machine.context.flags
if not (optional and subtoken_is_valid_flag):
token = orig
mutations = []
for index, value in mutations:
body.insert(index, value)
machine.handle(token)
machine.finish()
result = machine.result
result.remainder = " ".join(remainder)
return result
class ParseMachine(StateMachine):
initial_state = "context"
state("context", enter=["complete_flag", "complete_context"])
state("unknown", enter=["complete_flag", "complete_context"])
state("end", enter=["complete_flag", "complete_context"])
transition(from_=("context", "unknown"), event="finish", to="end")
transition(
from_="context",
event="see_context",
action="switch_to_context",
to="context",
)
transition(
from_=("context", "unknown"),
event="see_unknown",
action="store_only",
to="unknown",
)
def changing_state(self, from_: str, to: str) -> None:
debug("ParseMachine: {!r} => {!r}".format(from_, to))
def __init__(
self,
initial: "ParserContext",
contexts: Lexicon,
ignore_unknown: bool,
) -> None:
# Initialize
self.ignore_unknown = ignore_unknown
self.initial = self.context = copy.deepcopy(initial)
debug("Initialized with context: {!r}".format(self.context))
self.flag = None
self.flag_got_value = False
self.result = ParseResult()
self.contexts = copy.deepcopy(contexts)
debug("Available contexts: {!r}".format(self.contexts))
# In case StateMachine does anything in __init__
super().__init__()
@property
def waiting_for_flag_value(self) -> bool:
# Do we have a current flag, and does it expect a value (vs being a
# bool/toggle)?
takes_value = self.flag and self.flag.takes_value
if not takes_value:
return False
# OK, this flag is one that takes values.
# Is it a list type (which has only just been switched to)? Then it'll
# always accept more values.
# TODO: how to handle somebody wanting it to be some other iterable
# like tuple or custom class? Or do we just say unsupported?
if self.flag.kind is list and not self.flag_got_value:
return True
# Not a list, okay. Does it already have a value?
has_value = self.flag.raw_value is not None
# If it doesn't have one, we're waiting for one (which tells the parser
# how to proceed and typically to store the next token.)
# TODO: in the negative case here, we should do something else instead:
# - Except, "hey you screwed up, you already gave that flag!"
# - Overwrite, "oh you changed your mind?" - which requires more work
# elsewhere too, unfortunately. (Perhaps additional properties on
# Argument that can be queried, e.g. "arg.is_iterable"?)
return not has_value
def handle(self, token: str) -> None:
debug("Handling token: {!r}".format(token))
# Handle unknown state at the top: we don't care about even
# possibly-valid input if we've encountered unknown input.
if self.current_state == "unknown":
debug("Top-of-handle() see_unknown({!r})".format(token))
self.see_unknown(token)
return
# Flag
if self.context and token in self.context.flags:
debug("Saw flag {!r}".format(token))
self.switch_to_flag(token)
elif self.context and token in self.context.inverse_flags:
debug("Saw inverse flag {!r}".format(token))
self.switch_to_flag(token, inverse=True)
# Value for current flag
elif self.waiting_for_flag_value:
debug(
"We're waiting for a flag value so {!r} must be it?".format(
token
)
) # noqa
self.see_value(token)
# Positional args (must come above context-name check in case we still
# need a posarg and the user legitimately wants to give it a value that
# just happens to be a valid context name.)
elif self.context and self.context.missing_positional_args:
msg = "Context {!r} requires positional args, eating {!r}"
debug(msg.format(self.context, token))
self.see_positional_arg(token)
# New context
elif token in self.contexts:
self.see_context(token)
# Initial-context flag being given as per-task flag (e.g. --help)
elif self.initial and token in self.initial.flags:
debug("Saw (initial-context) flag {!r}".format(token))
flag = self.initial.flags[token]
# Special-case for core --help flag: context name is used as value.
if flag.name == "help":
flag.value = self.context.name
msg = "Saw --help in a per-task context, setting task name ({!r}) as its value" # noqa
debug(msg.format(flag.value))
# All others: just enter the 'switch to flag' parser state
else:
# TODO: handle inverse core flags too? There are none at the
# moment (e.g. --no-dedupe is actually 'no_dedupe', not a
# default-False 'dedupe') and it's up to us whether we actually
# put any in place.
self.switch_to_flag(token)
# Unknown
else:
if not self.ignore_unknown:
debug("Can't find context named {!r}, erroring".format(token))
self.error("No idea what {!r} is!".format(token))
else:
debug("Bottom-of-handle() see_unknown({!r})".format(token))
self.see_unknown(token)
def store_only(self, token: str) -> None:
# Start off the unparsed list
debug("Storing unknown token {!r}".format(token))
self.result.unparsed.append(token)
def complete_context(self) -> None:
debug(
"Wrapping up context {!r}".format(
self.context.name if self.context else self.context
)
)
# Ensure all of context's positional args have been given.
if self.context and self.context.missing_positional_args:
err = "'{}' did not receive required positional arguments: {}"
names = ", ".join(
"'{}'".format(x.name)
for x in self.context.missing_positional_args
)
self.error(err.format(self.context.name, names))
if self.context and self.context not in self.result:
self.result.append(self.context)
def switch_to_context(self, name: str) -> None:
self.context = copy.deepcopy(self.contexts[name])
debug("Moving to context {!r}".format(name))
debug("Context args: {!r}".format(self.context.args))
debug("Context flags: {!r}".format(self.context.flags))
debug("Context inverse_flags: {!r}".format(self.context.inverse_flags))
def complete_flag(self) -> None:
if self.flag:
msg = "Completing current flag {} before moving on"
debug(msg.format(self.flag))
# Barf if we needed a value and didn't get one
if (
self.flag
and self.flag.takes_value
and self.flag.raw_value is None
and not self.flag.optional
):
err = "Flag {!r} needed value and was not given one!"
self.error(err.format(self.flag))
# Handle optional-value flags; at this point they were not given an
# explicit value, but they were seen, ergo they should get treated like
# bools.
if self.flag and self.flag.raw_value is None and self.flag.optional:
msg = "Saw optional flag {!r} go by w/ no value; setting to True"
debug(msg.format(self.flag.name))
# Skip casting so the bool gets preserved
self.flag.set_value(True, cast=False)
def check_ambiguity(self, value: Any) -> bool:
"""
Guard against ambiguity when current flag takes an optional value.
.. versionadded:: 1.0
"""
# No flag is currently being examined, or one is but it doesn't take an
# optional value? Ambiguity isn't possible.
if not (self.flag and self.flag.optional):
return False
# We *are* dealing with an optional-value flag, but it's already
# received a value? There can't be ambiguity here either.
if self.flag.raw_value is not None:
return False
# Otherwise, there *may* be ambiguity if 1 or more of the below tests
# fail.
tests = []
# Unfilled posargs still exist?
tests.append(self.context and self.context.missing_positional_args)
# Value matches another valid task/context name?
tests.append(value in self.contexts)
if any(tests):
msg = "{!r} is ambiguous when given after an optional-value flag"
raise ParseError(msg.format(value))
def switch_to_flag(self, flag: str, inverse: bool = False) -> None:
# Sanity check for ambiguity w/ prior optional-value flag
self.check_ambiguity(flag)
# Also tie it off, in case prior had optional value or etc. Seems to be
# harmless for other kinds of flags. (TODO: this is a serious indicator
# that we need to move some of this flag-by-flag bookkeeping into the
# state machine bits, if possible - as-is it was REAL confusing re: why
# this was manually required!)
self.complete_flag()
# Set flag/arg obj
flag = self.context.inverse_flags[flag] if inverse else flag
# Update state
try:
self.flag = self.context.flags[flag]
except KeyError as e:
# Try fallback to initial/core flag
try:
self.flag = self.initial.flags[flag]
except KeyError:
# If it wasn't in either, raise the original context's
# exception, as that's more useful / correct.
raise e
debug("Moving to flag {!r}".format(self.flag))
# Bookkeeping for iterable-type flags (where the typical 'value
# non-empty/nondefault -> clearly it got its value already' test is
# insufficient)
self.flag_got_value = False
# Handle boolean flags (which can immediately be updated)
if self.flag and not self.flag.takes_value:
val = not inverse
debug("Marking seen flag {!r} as {}".format(self.flag, val))
self.flag.value = val
def see_value(self, value: Any) -> None:
self.check_ambiguity(value)
if self.flag and self.flag.takes_value:
debug("Setting flag {!r} to value {!r}".format(self.flag, value))
self.flag.value = value
self.flag_got_value = True
else:
self.error("Flag {!r} doesn't take any value!".format(self.flag))
def see_positional_arg(self, value: Any) -> None:
for arg in self.context.positional_args:
if arg.value is None:
arg.value = value
break
def error(self, msg: str) -> None:
raise ParseError(msg, self.context)