Skip to content

Commit

Permalink
Fixed missing plug-ins from vpype --help string (#444)
Browse files Browse the repository at this point in the history
When importing plug-ins in the style of `click-plugin` (command decorator, so plug-ins are loaded during the loading of the top-level command itself), plug-ins may not import from `vpype_cli` (since it isn't fully loaded). To address that, since 1.9, loading plug-ins is deferred to when `cli` is actually executed. As a result, the Click's default behaviour for handling `--help` (i.e. print and exit *before* even executing `cli`) is unable to list the plug-ins. This commit addresses this by manually handling the top-level `--help` parameter (*after* plug-ins are loaded).

Also fixed multiple plug-in loading when using `vpype_cli.execute()` multiple times

Fixes #432
  • Loading branch information
abey79 authored Apr 2, 2022
1 parent 11ebcf1 commit 7076c03
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 19 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ Release date: UNRELEASED

### Bug fixes

* Fixed issue with `forlayer` where the `_n` variable was set improperly (#443)
* Fixed issue with `write` where layer opacity was included in the `stroke` attribute instead of using `stroke-opacity`, which, although compliant, was not compatible with Inkscape (#429)
* Fixed an issue with `forlayer` where the `_n` variable was set improperly (#443)
* Fixed an issue with `write` where layer opacity was included in the `stroke` attribute instead of using `stroke-opacity`, which, although compliant, was not compatible with Inkscape (#429)
* Fixed an issue with `vpype --help` where commands from plug-ins would not be listed (#444)
* Fixed a minor issue where plug-ins would be reloaded each time `vpype_cli.execute()` is called (#444)


### API changes
Expand Down
6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,3 +719,11 @@ def test_pagerotate_error(caplog):
doc = vpype_cli.execute("random pagerotate")
assert doc.page_size is None
assert "page size is not defined, page not rotated" in caplog.text


def test_help(runner):
res = runner.invoke(cli, "--help")

assert res.exit_code == 0
assert "Execute the sequence of commands passed in argument." in res.stdout
assert "multipass" in res.stdout
55 changes: 41 additions & 14 deletions vpype_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,13 @@ def parse_args(self, ctx, args):
return args


# noinspection PyUnusedLocal,PyUnresolvedReferences
@click.group(cls=GroupedGroup, chain=True)
_PLUGINS_LOADED = False


# noinspection PyUnusedLocal
@click.group(cls=GroupedGroup, chain=True, invoke_without_command=True)
@click.version_option(version=vp.__version__, message="%(prog)s %(version)s")
@click.option("-h", "--help", "help_flag", is_flag=True, help="Show this message and exit.")
@click.option("-v", "--verbose", count=True)
@click.option("-I", "--include", type=click.Path(), help="Load commands from a command file.")
@click.option(
Expand All @@ -132,7 +136,15 @@ def parse_args(self, ctx, args):
"-c", "--config", type=click.Path(exists=True), help="Load an additional config file."
)
@click.pass_context
def cli(ctx, verbose, include, history, seed, config):
def cli(
ctx: click.Context,
help_flag: bool,
verbose: int,
include: bool,
history: bool,
seed: int,
config: str,
):
"""Execute the sequence of commands passed in argument.
The available commands are listed below. Information on each command may be obtained using:
Expand Down Expand Up @@ -183,15 +195,30 @@ def cli(ctx, verbose, include, history, seed, config):
# 1) Deferred plug-in loading avoid circular import between vpype and vpype_cli when plug-
# in uses deprecated APIs.
# 2) Avoids the PyCharm type error with CliRunner.invoke()
for entry_point in iter_entry_points("vpype.plugins"):
# noinspection PyBroadException
try:
ctx.command.add_command(entry_point.load())
except Exception:
# Catch this so a busted plugin doesn't take down the CLI.
# Handled by registering a dummy command that does nothing
# other than explain the error.
ctx.command.add_command(_BrokenCommand(entry_point.name))
global _PLUGINS_LOADED
if not _PLUGINS_LOADED:
_PLUGINS_LOADED = True
for entry_point in iter_entry_points("vpype.plugins"):
# noinspection PyBroadException
try:
cast(click.Group, ctx.command).add_command(entry_point.load())
except Exception:
# Catch this so a busted plugin doesn't take down the CLI.
# Handled by registering a dummy command that does nothing
# other than explain the error.
cast(click.Group, ctx.command).add_command(_BrokenCommand(entry_point.name))

# Manual handling of the help to work around circular import issues.
# Background: when importing plug-ins in the style of `click-plugin` (decorator, so plug-
# ins are loaded during the loading of `cli` itself), plug-in may not import things from
# `vpype_cli` since it is still partially loaded. Plug-ins are thus loaded when `cli` is
# actually executed (see previous lines). As a result, the Click's default behaviour for
# handling `--help` (i.e. print and exit *before* even executing `cli`) is unable to list
# the plug-ins. This is addressed by manually handling the top-level `--help` parameter
# *after* plug-ins are loaded.
if help_flag:
print(ctx.command.get_help(ctx))
sys.exit(0)

# We use the command string as context object, mainly for the purpose of the `write`
# command. This is a bit of a hack, and will need to be updated if we ever need more state
Expand All @@ -218,9 +245,9 @@ def cli(ctx, verbose, include, history, seed, config):
cli = cast(GroupedGroup, cli)


# noinspection PyShadowingNames,PyUnusedLocal
# noinspection PyUnusedLocal
@cli.result_callback()
def process_pipeline(processors, verbose, include, history, seed, config):
def process_pipeline(processors, help_flag, verbose, include, history, seed, config):
execute_processors(processors, State())


Expand Down

0 comments on commit 7076c03

Please sign in to comment.