Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add FORWARD parameter type and stop parsing arguments when FORWARD is… #2686

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
from .types import File as File
from .types import FLOAT as FLOAT
from .types import FloatRange as FloatRange
from .types import FORWARD as FORWARD
from .types import INT as INT
from .types import IntRange as IntRange
from .types import ParamType as ParamType
Expand Down
14 changes: 14 additions & 0 deletions src/click/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from .exceptions import BadOptionUsage
from .exceptions import NoSuchOption
from .exceptions import UsageError
from .types import FORWARD

if t.TYPE_CHECKING:
from .core import Argument as CoreArgument
Expand Down Expand Up @@ -321,6 +322,16 @@ def _process_args_for_args(self, state: _ParsingState) -> None:
state.largs = args
state.rargs = []

def _stop_process_args_for_options(self, state: _ParsingState) -> bool:
largs: t.Sequence[str] = state.largs
for args in self._args:
if args.obj.type == FORWARD and args.nargs < 0:
return True
if not largs:
break
largs = largs[args.nargs :]
return False

def _process_args_for_options(self, state: _ParsingState) -> None:
while state.rargs:
arg = state.rargs.pop(0)
Expand All @@ -331,6 +342,9 @@ def _process_args_for_options(self, state: _ParsingState) -> None:
return
elif arg[:1] in self._opt_prefixes and arglen > 1:
self._process_opts(arg, state)
elif self._stop_process_args_for_options(state):
state.rargs.insert(0, arg)
return
elif self.allow_interspersed_args:
state.largs.append(arg)
else:
Expand Down
16 changes: 16 additions & 0 deletions src/click/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,18 @@ def __repr__(self) -> str:
return "UNPROCESSED"


class ForwardParamType(ParamType):
name = "text"

def convert(
self, value: t.Any, param: Parameter | None, ctx: Context | None
) -> t.Any:
return value

def __repr__(self) -> str:
return "FORWARD"


class StringParamType(ParamType):
name = "text"

Expand Down Expand Up @@ -1071,6 +1083,10 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType:
#: .. versionadded:: 4.0
UNPROCESSED = UnprocessedParamType()

#: A dummy parameter type that just does nothing except stops parsing options
#: and arguments when this argument is getting parsed.
FORWARD = ForwardParamType()

#: A unicode string parameter type which is the implicit default. This
#: can also be selected by using ``str`` as type.
STRING = StringParamType()
Expand Down
47 changes: 47 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,53 @@ def cli(verbose, args):
]


def test_forward_options(runner):
@click.command()
@click.option("-f")
@click.argument("files", nargs=-1, type=click.FORWARD)
def cmd(f, files):
click.echo(f)
for filename in files:
click.echo(filename)

args = ["echo", "-foo", "bar", "-f", "-h", "--help"]
result = runner.invoke(cmd, args)
assert result.output.splitlines() == [""] + args


def test_forward_options_group(runner):
@click.group()
@click.option("-f")
def cmd(f):
click.echo(f)

@cmd.command()
@click.option("-a")
@click.argument("src", nargs=1)
@click.argument("dsts", nargs=-1, type=click.FORWARD)
def cp(a, src, dsts):
click.echo(a)
click.echo(src)
for dst in dsts:
click.echo(dst)

result = runner.invoke(
cmd,
["-f", "f", "cp", "-a", "a", "src", "dst1", "-a", "dst2", "-h", "--help", "-f"],
)
assert result.output.splitlines() == [
"f",
"a",
"src",
"dst1",
"-a",
"dst2",
"-h",
"--help",
"-f",
]


@pytest.mark.parametrize("doc", ["CLI HELP", None])
def test_deprecated_in_help_messages(runner, doc):
@click.command(deprecated=True, help=doc)
Expand Down
Loading