diff --git a/mknodes/basenodes/mkclidoc/__init__.py b/mknodes/basenodes/mkclidoc/__init__.py index 0c0049d3..0b744722 100644 --- a/mknodes/basenodes/mkclidoc/__init__.py +++ b/mknodes/basenodes/mkclidoc/__init__.py @@ -4,7 +4,8 @@ from mknodes.templatenodes import mktemplate from mknodes.utils import log -from mknodes.info.cli import clihelpers, commandinfo + +import clinspector if TYPE_CHECKING: import argparse @@ -42,7 +43,7 @@ def __init__( self.show_subcommands = show_subcommands @property - def info(self) -> commandinfo.CommandInfo | None: + def info(self) -> clinspector.CommandInfo | None: import importlib match self.target: @@ -62,7 +63,7 @@ def info(self) -> commandinfo.CommandInfo | None: case _: instance = self.target prog_name = self.prog_name - return clihelpers.get_cli_info(instance, command=prog_name) + return clinspector.get_cmd_info(instance, command=prog_name) if __name__ == "__main__": diff --git a/mknodes/info/cli/__init__.py b/mknodes/info/cli/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/mknodes/info/cli/clihelpers.py b/mknodes/info/cli/clihelpers.py deleted file mode 100644 index 5b712aeb..00000000 --- a/mknodes/info/cli/clihelpers.py +++ /dev/null @@ -1,117 +0,0 @@ -from __future__ import annotations - -import argparse -from typing import TYPE_CHECKING - -from mknodes.info.cli import commandinfo, param - - -if TYPE_CHECKING: - import click - import typer - - -def get_argparse_info(parser: argparse.ArgumentParser): - # subs = [ - # commandinfo.CommandInfo(i) - # for i in parser._actions - # if isinstance(i, argparse._SubParsersAction) - # ] - params = [ - param.Param( - metavar=" ".join(i.metavar) if isinstance(i.metavar, tuple) else i.metavar, - help=i.help, - default=i.default if i.default != argparse.SUPPRESS else None, - opts=list(i.option_strings), - nargs=i.nargs, - required=i.required, - # dest: str - # const: Any - # choices: Iterable[Any] | None - ) - for i in parser._actions - ] - return commandinfo.CommandInfo( - name=parser.prog, - description=parser.description or "", - usage=parser.format_usage(), - params=params, - # subcommands={i.name: get_argparse_info(i) for i in parser._subparsers}, - ) - - -def get_cli_info( - instance: typer.Typer | click.Group | argparse.ArgumentParser, - command: str | None = None, -) -> commandinfo.CommandInfo: - """Return a `CommmandInfo` object for command of given instance. - - Instance can either be a click Group, a Typer instance or an ArgumentParser - - Args: - instance: A `Typer`, **click** `Group` or `ArgumentParser` instance - command: The command to get info for. - """ - if isinstance(instance, argparse.ArgumentParser): - info = get_argparse_info(instance) - return info[command] if command else info - - import typer - from typer.main import get_command - - cmd = get_command(instance) if isinstance(instance, typer.Typer) else instance - info = get_click_info(cmd) - if command: - ctx = typer.Context(cmd) - subcommands = getattr(cmd, "commands", {}) - cmds = {k: get_click_info(v, parent=ctx) for k, v in subcommands.items()} - return cmds.get(command, info) - return info - - -def get_click_info( - command: click.Command, - parent: click.Context | None = None, -) -> commandinfo.CommandInfo: - """Get a `CommandInfo` dataclass for given click `Command`. - - Args: - command: The **click** `Command` to get info for. - parent: The optional parent context - """ - import click - - ctx = click.Context(command, parent=parent) - subcommands = getattr(command, "commands", {}) - dct = ctx.command.to_info_dict(ctx) - formatter = ctx.make_formatter() - pieces = ctx.command.collect_usage_pieces(ctx) - formatter.write_usage(ctx.command_path, " ".join(pieces), prefix="") - usage = formatter.getvalue().rstrip("\n") - # Generate the full usage string based on parents if any, i.e. `root sub1 sub2 ...`. - full_path = [] - current: click.Context | None = ctx - while current is not None: - name = current.command.name.lower() if current.command.name else "" - full_path.append(name) - current = current.parent - full_path.reverse() - return commandinfo.CommandInfo( - name=ctx.command.name or "", - description=ctx.command.help or ctx.command.short_help or "", - usage=" ".join(full_path) + usage, - params=[param.Param(**i) for i in dct["params"]], - subcommands={k: get_click_info(v, parent=ctx) for k, v in subcommands.items()}, - deprecated=dct["deprecated"], - epilog=dct["epilog"], - hidden=dct["hidden"], - ) - - -if __name__ == "__main__": - from pprint import pprint - - import mkdocs.__main__ - - info = get_cli_info(mkdocs.__main__.cli, command="mkdocs") - pprint(info) diff --git a/mknodes/info/cli/commandinfo.py b/mknodes/info/cli/commandinfo.py deleted file mode 100644 index 2138b1ad..00000000 --- a/mknodes/info/cli/commandinfo.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import annotations - -import dataclasses -from typing import TYPE_CHECKING - -from mknodes.utils import reprhelpers - - -if TYPE_CHECKING: - from mknodes.info.cli import param - - -@dataclasses.dataclass(frozen=True) -class CommandInfo: - name: str - """The name of the command.""" - description: str - """A description for this command.""" - usage: str = "" - """A formatted string containing a formatted "usage string" (placeholder example)""" - subcommands: dict[str, CommandInfo] = dataclasses.field(default_factory=dict) - """A command-name->CommandInfo mapping containing all subcommands.""" - deprecated: bool = False - """Whether this command is deprecated.""" - epilog: str | None = None - """Epilog for this command.""" - hidden: bool = False - """Whether this command is hidden.""" - params: list[param.Param] = dataclasses.field(default_factory=list) - """A list of Params for this command.""" - - def __getitem__(self, name): - return self.subcommands[name] - - def __repr__(self): - return reprhelpers.get_dataclass_repr(self) - - -if __name__ == "__main__": - from pprint import pprint - - info = CommandInfo("A", "B", "C") - pprint(info) diff --git a/mknodes/info/cli/param.py b/mknodes/info/cli/param.py deleted file mode 100644 index d4eef867..00000000 --- a/mknodes/info/cli/param.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import annotations - -import dataclasses -from typing import TYPE_CHECKING, Any, Literal - -from mknodes.utils import reprhelpers - - -if TYPE_CHECKING: - from collections.abc import Callable - - -@dataclasses.dataclass(frozen=True) -class Param: - count: bool = False - """Whether the parameter increments an integer.""" - default: Any = None - """The default value of the parameter.""" - envvar: str | None = None - """ the environment variable name for this parameter.""" - flag_value: bool = False - """ Value used for this flag if enabled.""" - help: str | None = None - """A formatted help text for this parameter.""" - hidden: bool = False - """Whether this parameter is hidden.""" - is_flag: bool = False - """Whether the parameter is a flag.""" - multiple: bool = False - """Whether the parameter is accepted multiple times and recorded.""" - name: str = "" - """The name of this parameter.""" - nargs: int | str | None = 1 - """The number of arguments this parameter matches. (Argparse may return * / ?)""" - opts: list[str] = dataclasses.field(default_factory=list) - """Options for this parameter.""" - param_type_name: Literal["option", "parameter", "argument"] = "option" - """The type of the parameter.""" - prompt: Any = None - """Whether user is prompted for this parameter.""" - required: bool = False - """Whether the parameter is required.""" - secondary_opts: list[str] = dataclasses.field(default_factory=list) - """Secondary options for this parameter.""" - type: dict[str, str] | None = None - """The type object of the parameter.""" - callback: Callable[[Any], Any] | None = None - """A method to further process the value after type conversion.""" - expose_value: str = "" - """Whether value is passed onwards to the command callback and stored in context.""" - is_eager: bool = False - """Whether the param is eager.""" - metavar: str | None = None - """How value is represented in the help page.""" - - @property - def opt_str(self) -> str: - """A formatted and sorted string containing the the options.""" - return ", ".join(f"`{i}`" for i in reversed(self.opts)) - - def __repr__(self): - return reprhelpers.get_dataclass_repr(self) - - -if __name__ == "__main__": - from pprint import pprint - - info = Param(name="test") - pprint(info) diff --git a/mknodes/info/contexts.py b/mknodes/info/contexts.py index 4cdb75a6..7757bd74 100644 --- a/mknodes/info/contexts.py +++ b/mknodes/info/contexts.py @@ -26,10 +26,9 @@ import datetime import types + import clinspector from griffe import Alias, Module - from mknodes.info.cli import commandinfo - logger = log.get_logger(__name__) @@ -211,7 +210,7 @@ class PackageContext(Context): """A dictionary containing the entry points of the distribution.""" cli: str | None = None """The cli package name used by the distribution.""" - cli_info: commandinfo.CommandInfo | None = None + cli_info: clinspector.CommandInfo | None = None """An object containing information about all cli commands.""" # required_packages: dict[PackageInfo, packagehelpers.Dependency] = diff --git a/mknodes/info/packageinfo.py b/mknodes/info/packageinfo.py index 65376d73..e31248e2 100644 --- a/mknodes/info/packageinfo.py +++ b/mknodes/info/packageinfo.py @@ -6,10 +6,10 @@ from importlib import metadata from typing import Any +import clinspector import epregistry from requests import structures -from mknodes.info.cli import clihelpers, commandinfo from mknodes.utils import log, packagehelpers, reprhelpers @@ -202,13 +202,13 @@ def cli(self) -> str | None: return None @functools.cached_property - def cli_info(self) -> commandinfo.CommandInfo | None: + def cli_info(self) -> clinspector.CommandInfo | None: """Return a CLI info object containing infos about all CLI commands / options.""" if eps := self.entry_points.get_group("console_scripts"): ep = eps[0].load() qual_name = ep.__class__.__module__.lower() if qual_name.startswith(("typer", "click")): - return clihelpers.get_cli_info(ep) + return clinspector.get_cmd_info(ep) return None @functools.cached_property diff --git a/pyproject.toml b/pyproject.toml index 5dd61e84..bedaa60e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ dependencies = [ "git-changelog", "mkdocstrings[python]", "epregistry", + "clinspector", ] license = { file = "LICENSE" }