Skip to content

Commit

Permalink
feat: ape plugins list section-header fix AND allowing `--format fr…
Browse files Browse the repository at this point in the history
…eeze` option (#2502)
  • Loading branch information
antazoey authored Feb 12, 2025
1 parent db798c0 commit f86b325
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 30 deletions.
106 changes: 90 additions & 16 deletions src/ape/plugins/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"notebook",
"optimism",
"polygon",
"polygon_zkevm",
"polygon-zkevm",
"safe",
"solidity",
"template",
Expand All @@ -91,6 +91,12 @@ def get_plugin_dists():
return _filter_plugins_from_dists(_get_distributions())


class OutputFormat(str, Enum):
DEFAULT = "default"
PREFIXED = "prefixed"
FREEZE = "freeze"


def _filter_plugins_from_dists(dists: Iterable) -> Iterator[str]:
for dist in dists:
if name := getattr(dist, "name", ""):
Expand Down Expand Up @@ -214,7 +220,10 @@ def load(cls, plugin_manager: "PluginManager", include_available: bool = True):

@classmethod
def from_package_names(
cls, packages: Iterable[str], include_available: bool = True
cls,
packages: Iterable[str],
include_available: bool = True,
trusted_list: Optional[Iterable] = None,
) -> "PluginMetadataList":
PluginMetadataList.model_rebuild()
core = PluginGroup(plugin_type=PluginType.CORE)
Expand All @@ -232,14 +241,15 @@ def from_package_names(

# perf: only check these once.
is_installed = plugin.is_installed
is_trusted = plugin.check_trusted(use_web=False, trusted_list=trusted_list)
is_available = include_available and plugin.is_available

if include_available and is_available and not is_installed:
available.plugins[name] = plugin
elif is_installed and not plugin.in_core and not is_available:
third_party.plugins[name] = plugin
elif is_installed:
elif is_installed and not plugin.in_core and is_trusted:
installed.plugins[name] = plugin
elif is_installed:
third_party.plugins[name] = plugin
else:
logger.error(f"'{plugin.name}' is not a plugin.")

Expand All @@ -248,8 +258,14 @@ def from_package_names(
def __str__(self) -> str:
return self.to_str()

def to_str(self, include: Optional[Sequence[PluginType]] = None) -> str:
return str(ApePluginsRepr(self, include=include))
def to_str(
self,
include: Optional[Sequence[PluginType]] = None,
include_version: bool = True,
output_format: OutputFormat = OutputFormat.DEFAULT,
) -> str:
representation = ApePluginsRepr(self, include=include, output_format=output_format)
return str(representation)

@property
def all_plugins(self) -> Iterator["PluginMetadata"]:
Expand Down Expand Up @@ -457,14 +473,14 @@ def check_installed(self, use_cache: bool = True) -> bool:

return any(n == self.package_name for n in get_plugin_dists())

def check_trusted(self, use_web: bool = True) -> bool:
def check_trusted(self, use_web: bool = True, trusted_list: Optional[Iterable] = None) -> bool:
if use_web:
return self.is_available

else:
# Sometimes (such as for --help commands), it is better
# to not check GitHub to see if the plugin is trusted.
return self.name in TRUSTED_PLUGINS
# Sometimes (such as for --help commands), it is better
# to not check GitHub to see if the plugin is trusted.
trusted_list = trusted_list or TRUSTED_PLUGINS
return self.name in trusted_list

def _prepare_install(
self, upgrade: bool = False, skip_confirmation: bool = False
Expand Down Expand Up @@ -620,6 +636,9 @@ def __repr__(self) -> str:
def __str__(self) -> str:
return self.to_str()

def __iter__(self) -> Iterator[str]: # type: ignore
yield from self.plugins

@field_validator("plugin_type", mode="before")
@classmethod
def validate_plugin_type(cls, value):
Expand All @@ -637,7 +656,31 @@ def name(self) -> str:
def plugin_names(self) -> list[str]:
return [x.name for x in self.plugins.values()]

def to_str(self, max_length: Optional[int] = None, include_version: bool = True) -> str:
def to_str(
self,
max_length: Optional[int] = None,
include_version: bool = True,
output_format: Optional[OutputFormat] = OutputFormat.DEFAULT,
) -> str:
output_format = output_format or OutputFormat.DEFAULT
if output_format in (OutputFormat.DEFAULT, OutputFormat.PREFIXED):
return self._get_default_formatted_str(
max_length=max_length,
include_version=include_version,
include_prefix=f"{output_format}" == OutputFormat.PREFIXED,
)

# Freeze format.
return self._get_freeze_formatted_str(
max_length=max_length, include_version=include_version
)

def _get_default_formatted_str(
self,
max_length: Optional[int] = None,
include_version: bool = True,
include_prefix: bool = False,
) -> str:
title = f"{self.name} Plugins"
if len(self.plugins) <= 0:
return title
Expand All @@ -646,7 +689,7 @@ def to_str(self, max_length: Optional[int] = None, include_version: bool = True)
max_length = self.max_name_length if max_length is None else max_length
plugins_sorted = sorted(self.plugins.values(), key=lambda p: p.name)
for plugin in plugins_sorted:
line = plugin.name
line = plugin.package_name if include_prefix else plugin.name
if include_version:
version = plugin.version or get_package_version(plugin.package_name)
if version:
Expand All @@ -657,6 +700,23 @@ def to_str(self, max_length: Optional[int] = None, include_version: bool = True)

return "\n".join(lines)

def _get_freeze_formatted_str(
self,
max_length: Optional[int] = None,
include_version: bool = True,
include_prefix: bool = False,
) -> str:
lines = []
for plugin in sorted(self.plugins.values(), key=lambda p: p.name):
line = plugin.package_name
if include_version:
version = plugin.version or get_package_version(plugin.package_name)
line = f"{line}=={version}"

lines.append(line)

return "\n".join(lines)

@property
def max_name_length(self) -> int:
if not self.plugins:
Expand All @@ -671,10 +731,16 @@ class ApePluginsRepr:
"""

def __init__(
self, metadata: PluginMetadataList, include: Optional[Sequence[PluginType]] = None
self,
metadata: PluginMetadataList,
include: Optional[Sequence[PluginType]] = None,
include_version: bool = True,
output_format: Optional[OutputFormat] = None,
):
self.include = include or (PluginType.INSTALLED, PluginType.THIRD_PARTY)
self.metadata = metadata
self.include_version = include_version
self.output_format = output_format

@log_instead_of_fail(default="<ApePluginsRepr>")
def __repr__(self) -> str:
Expand All @@ -700,8 +766,16 @@ def __str__(self) -> str:
max_length = max(x.max_name_length for x in sections)

version_skips = (PluginType.CORE, PluginType.AVAILABLE)

def include_version(section):
return section.plugin_type not in version_skips if self.include_version else False

formatted_sections = [
x.to_str(max_length=max_length, include_version=x.plugin_type not in version_skips)
x.to_str(
max_length=max_length,
include_version=include_version(x),
output_format=self.output_format,
)
for x in sections
]
return "\n\n".join(formatted_sections)
13 changes: 11 additions & 2 deletions src/ape_plugins/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,21 @@ def _display_all_callback(ctx, param, value):
callback=_display_all_callback,
help="Display all plugins installed and available (including Core)",
)
def _list(cli_ctx, to_display):
@click.option(
"--format",
"output_format",
type=click.Choice(["default", "prefixed", "freeze"]),
default="default",
)
@click.option("--exclude-version", is_flag=True, help="Do not include plugin versions in output")
def _list(cli_ctx, to_display, output_format, exclude_version):
from ape.plugins._utils import PluginMetadataList, PluginType

include_available = PluginType.AVAILABLE in to_display
metadata = PluginMetadataList.load(cli_ctx.plugin_manager, include_available=include_available)
if output := metadata.to_str(include=to_display):
if output := metadata.to_str(
include=to_display, include_version=not exclude_version, output_format=output_format
):
click.echo(output)
if not metadata.installed and not metadata.third_party:
click.echo("No plugins installed (besides core plugins).")
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def plugin_metadata(package_names, plugin_test_env) -> PluginMetadataList:
names.add(f"ape-installed==0.{ape_version.minor}.0")
names.remove("ape-thirdparty")
names.add(f"ape-thirdparty==0.{ape_version.minor}.0")
return PluginMetadataList.from_package_names(names)
return PluginMetadataList.from_package_names(names, trusted_list=("installed",))


class TestPluginMetadataList:
Expand Down
32 changes: 21 additions & 11 deletions tests/integration/cli/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class ListResult:
def __init__(self, lines: list[str]):
self._lines = lines

def __str__(self) -> str:
return "\n".join(self._lines)

@classmethod
def parse_output(cls, output: str) -> "ListResult":
lines = [x.strip() for x in output.split("\n") if x.strip()]
Expand Down Expand Up @@ -89,22 +92,17 @@ def installed_plugin(ape_plugins_runner):
plugin_installed = TEST_PLUGIN_NAME in ape_plugins_runner.invoke_list().installed_plugins
did_install = False
if not plugin_installed:
install_result = ape_plugins_runner.invoke(
(
"install",
TEST_PLUGIN_NAME,
)
)
install_result = ape_plugins_runner.invoke("install", TEST_PLUGIN_NAME)
list_result = ape_plugins_runner.invoke_list()
plugins_list_output = list_result.installed_plugins
did_install = TEST_PLUGIN_NAME in plugins_list_output
msg = f"Failed to install plugin necessary for tests: {install_result.output}"
assert did_install, msg

yield
yield TEST_PLUGIN_NAME

if did_install:
ape_plugins_runner.invoke(("uninstall", TEST_PLUGIN_NAME))
ape_plugins_runner.invoke("uninstall", TEST_PLUGIN_NAME)


@github_xfail()
Expand Down Expand Up @@ -132,24 +130,36 @@ def test_list_does_not_repeat(ape_plugins_runner, installed_plugin):
assert "ethereum" not in result.available_plugins


@github_xfail()
def test_list_prefixed_format(ape_plugins_runner, installed_plugin):
result = ape_plugins_runner.invoke_list(("--format", "prefixed"))
assert f"ape-{installed_plugin}" in result.installed_plugins


@github_xfail()
def test_list_freeze_format(ape_plugins_runner, installed_plugin):
result = ape_plugins_runner.invoke_list(("--format", "freeze"))
assert f"ape-{installed_plugin}==" in f"{result}"


@pytest.mark.pip
@run_once
def test_install_upgrade(ape_plugins_runner, installed_plugin):
result = ape_plugins_runner.invoke(("install", TEST_PLUGIN_NAME, "--upgrade"))
result = ape_plugins_runner.invoke("install", TEST_PLUGIN_NAME, "--upgrade")
assert result.exit_code == 0


@pytest.mark.pip
@run_once
def test_install_upgrade_failure(ape_plugins_runner):
result = ape_plugins_runner.invoke(("install", "NOT_EXISTS", "--upgrade"))
result = ape_plugins_runner.invoke("install", "NOT_EXISTS", "--upgrade")
assert result.exit_code == 1


@pytest.mark.pip
@run_once
def test_install_multiple_in_one_str(ape_plugins_runner):
result = ape_plugins_runner.invoke(("install", f"{TEST_PLUGIN_NAME} {TEST_PLUGIN_NAME_2}"))
result = ape_plugins_runner.invoke("install", f"{TEST_PLUGIN_NAME} {TEST_PLUGIN_NAME_2}")
assert result.exit_code == 0


Expand Down

0 comments on commit f86b325

Please sign in to comment.