Skip to content

Commit

Permalink
📝 Add docstrings to simpler-syntax
Browse files Browse the repository at this point in the history
Docstrings generation was requested by @jaykv.

* #39 (comment)

The following files were modified:

* `cliffy/commander.py`
* `cliffy/commanders/typer.py`
* `cliffy/manifest.py`
* `cliffy/parser.py`
* `tests/test_commander.py`
* `tests/test_manifest.py`
  • Loading branch information
coderabbitai[bot] authored and jaykv committed Jan 14, 2025
1 parent f52dd82 commit 2fdf3cb
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 4 deletions.
46 changes: 46 additions & 0 deletions cliffy/commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ def setup_command_aliases(self) -> None:
self.aliases_by_commands[command.name].append(alias)

def build_groups(self) -> None:
"""
Organize commands into groups and process command templates and aliases.
This method performs several key operations:
- Separates greedy commands for later processing
- Merges command templates with individual commands
- Handles command aliases
- Creates command groups based on hierarchical command names
Side Effects:
- Populates self.greedy with greedy commands
- Populates self.groups with organized command groups
- Updates command attributes with template configurations
- Tracks command aliases in self.aliases_by_commands
Raises:
ValueError: If a referenced command template is undefined
"""
groups: DefaultDict[str, list[Command]] = defaultdict(list)
group_help_dict = {}

Expand Down Expand Up @@ -237,6 +255,24 @@ def add_main_block(self) -> None:
raise NotImplementedError

def from_greedy_make_lazy_command(self, greedy_command: Command, group: str) -> Command:
"""
Convert a greedy command to a lazy command by replacing placeholders with a specific group name.
This method creates a deep copy of the input greedy command and replaces all occurrences of greedy placeholders
('{(*)}' and '(*)') with the provided group name across various command attributes.
Parameters:
greedy_command (Command): The original greedy command to be transformed
group (str): The group name to replace placeholders with
Returns:
Command: A new command with placeholders replaced by the group name, ready for lazy loading
Notes:
- Handles replacement in command name, run blocks, help text, template, pre-run, and post-run blocks
- Supports different parameter types: GenericCommandParam, CommandParam, and SimpleCommandParam
- Preserves the original command's structure while updating placeholder-based content
"""
lazy_command = greedy_command.model_copy(deep=True)
lazy_command.name = greedy_command.name.replace("(*)", group)
if isinstance(lazy_command.run, RunBlock):
Expand Down Expand Up @@ -283,6 +319,16 @@ def from_greedy_make_lazy_command(self, greedy_command: Command, group: str) ->


def generate_cli(manifest: CLIManifest, commander_cls: type[Commander] = Commander) -> CLI:
"""
Generate a CLI object from a CLI manifest using the specified commander class.
Parameters:
manifest (CLIManifest): The manifest containing CLI configuration details
commander_cls (type[Commander], optional): Commander class to use for CLI generation. Defaults to Commander.
Returns:
CLI: A CLI object with generated code, name, version, and required dependencies
"""
commander = commander_cls(manifest)
commander.generate_cli()
return CLI(name=manifest.name, version=manifest.version, code=commander.cli, requires=manifest.requires)
27 changes: 27 additions & 0 deletions cliffy/commanders/typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ def main("""
"""

def add_root_command(self, command: Command) -> None:
"""
Add a root command to the CLI application with optional aliases.
This method dynamically generates a command function for the Typer CLI based on the provided Command configuration. It handles command registration, help text, and optional command aliases.
Parameters:
command (Command): The command configuration to be added as a root command.
Notes:
- Skips command generation if no run method is defined
- Supports command help text and configuration
- Creates hidden alias commands for the primary command
- Integrates with the Typer CLI framework
"""
if not command.run:
return

Expand All @@ -97,6 +111,19 @@ def add_group(self, group: Group) -> None:
"""

def add_sub_command(self, command: Command, group: Group) -> None:
"""
Add a sub-command to a Typer CLI group with support for command configuration, help text, and aliases.
Parameters:
command (Command): The command to be added as a sub-command
group (Group): The group to which the sub-command belongs
This method dynamically generates a function for the sub-command, registers it with the specified group's Typer app,
and adds any defined aliases as hidden commands. The function handles parsing command parameters, help text,
and configuration settings.
The generated sub-command can be invoked directly or through its aliases, with optional help text and configuration.
"""
parsed_command_func_name = self.parser.get_command_func_name(command)
parsed_command_name = self.parser.get_parsed_command_name(command)
parsed_command_config = self.parser.get_parsed_config(command)
Expand Down
62 changes: 62 additions & 0 deletions cliffy/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,43 @@ class CommandParam(BaseModel):
@field_validator("short", mode="after")
@classmethod
def short_only_with_option(cls, v: str, info: ValidationInfo) -> str:
"""
Validates that a short parameter alias is only used when the parameter name is a flag (starts with `--`).
Parameters:
cls (type): The class being validated (automatically passed by Pydantic).
v (str): The short alias value to validate.
info (ValidationInfo): Validation context containing the parameter data.
Returns:
str: The original short alias if validation passes.
Raises:
ValueError: If a short alias is provided without a flag-style parameter name.
Example:
# Valid: Short alias with flag parameter
param = CommandParam(name='--verbose', short='v')
# Invalid: Short alias without flag parameter
param = CommandParam(name='verbose', short='v') # Raises ValueError
"""
if v and not info.data.get("name", "").startswith("--"):
raise ValueError("short can only be used when name is prefixed as flag: `--`.")
return v

def is_option(self) -> bool:
"""
Determines whether the command parameter represents a command-line option.
Returns:
bool: True if the parameter name starts with '--', indicating it is a command-line option; False otherwise.
Examples:
- '--verbose' returns True
- 'input' returns False
- ' --debug ' returns True (whitespace is stripped before checking)
"""
return self.name.strip().startswith("--")


Expand Down Expand Up @@ -286,6 +318,26 @@ def get_field_description(cls, field_name: str, as_comment: bool = True) -> str:

@classmethod
def get_template(cls, cli_name: str, json_schema: bool) -> str:
"""
Generate a template for a CLI manifest with optional JSON schema reference.
This method creates a comprehensive YAML template for a CLI application, providing a structured
outline with placeholders and example configurations for various manifest components.
Parameters:
cls (type): The class on which the method is called (typically CLIManifest).
cli_name (str): The name of the CLI, which must be a valid Python identifier.
json_schema (bool): Flag to include a JSON schema reference comment.
Returns:
str: A YAML-formatted manifest template with example configurations.
Raises:
ValueError: If the provided CLI name is not a valid Python identifier.
Example:
manifest = CLIManifest.get_template("mycli", json_schema=True)
"""
if not cli_name.isidentifier():
raise ValueError("CLI name must be a valid Python identifier")

Expand Down Expand Up @@ -400,6 +452,16 @@ def save_data(data):

@classmethod
def get_raw_template(cls, cli_name: str, json_schema: bool) -> str:
"""
Generate a raw YAML template for a CLI manifest.
Parameters:
cli_name (str): The name of the CLI to be used in the template
json_schema (bool): Flag to include JSON schema reference comment
Returns:
str: A YAML-formatted template for a CLI manifest with default configuration
"""
manifest = ""
if json_schema:
manifest += "# yaml-language-server: $schema=cliffy_schema.json\n"
Expand Down
81 changes: 81 additions & 0 deletions cliffy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ def build_param_type(
help: Optional[str] = None,
extra_params: Optional[str] = None,
) -> str:
"""
Builds a type-annotated parameter definition for Typer CLI command.
Parameters:
param_name (str): Name of the parameter to be defined.
param_type (str): Type annotation for the parameter.
typer_cls (str): Typer class to use (e.g., "Option", "Argument").
aliases (list[str], optional): Alternative names for the parameter.
default_val (Any, optional): Default value for the parameter.
is_required (bool, default=False): Whether the parameter is mandatory.
help (str, optional): Help text describing the parameter.
extra_params (str, optional): Additional Typer parameter configurations.
Returns:
str: A formatted parameter type definition for Typer CLI command generation.
Example:
# Generates: username: str = typer.Option("", "--username", help="User login")
build_param_type(
param_name="username",
param_type="str",
typer_cls="Option",
default_val="",
help="User login"
)
"""
parsed_param_type = f"{param_name}: {param_type} = typer.{typer_cls}"
if not default_val:
# Required param needs ...
Expand All @@ -95,6 +121,26 @@ def build_param_type(
return parsed_param_type

def parse_param(self, param_name: str, param_type: str) -> str:
"""
Parse a single command parameter, determining its type, required status, and formatting.
Processes a parameter by extracting its characteristics such as required status, default value,
and type. Handles parameter aliases, option/argument classification, and type resolution.
Parameters:
param_name (str): The name of the parameter to parse.
param_type (str): The type definition of the parameter.
Returns:
str: A formatted parameter definition for use in command parsing.
Notes:
- Strips trailing '!' to indicate required parameters
- Replaces dashes with underscores in parameter names
- Supports parameter aliases for options
- Resolves parameter types from a predefined manifest
- Handles default values and optional/required status
"""
is_required = self.is_param_required(param_type)
default_val = self.get_default_param_val(param_type)
param_typer_cls = "Option" if self.is_param_option(param_name) else "Argument"
Expand Down Expand Up @@ -129,6 +175,25 @@ def parse_param(self, param_name: str, param_type: str) -> str:
)

def parse_params(self, command: Command) -> str:
"""
Parse command parameters for a given command, combining global and command-specific parameters.
Processes a list of command parameters, handling different parameter types including CommandParam,
GenericCommandParam, and SimpleCommandParam. Generates a formatted string representation of parameters
suitable for command definition.
Parameters:
command (Command): The command whose parameters are to be parsed.
Returns:
str: A formatted string of parsed command parameters, or an empty string if no parameters exist.
Notes:
- Combines global parameters with command-specific parameters
- Supports different parameter types with varying parsing strategies
- Handles parameter aliases, help text, default values, and required status
- Strips trailing whitespace and comma from the final parameter string
"""
if not command.params:
return ""

Expand Down Expand Up @@ -164,6 +229,22 @@ def parse_params(self, command: Command) -> str:
return parsed_command_params[:-2]

def get_parsed_config(self, command: Command) -> str:
"""
Retrieve and format the configuration options for a given command.
Converts the command's configuration to a string of command-line arguments, excluding any unset options.
Parameters:
command (Command): The command whose configuration is to be parsed.
Returns:
str: A formatted string of command-line arguments representing the command's configuration,
or an empty string if no configuration is set.
Example:
If a command has a configuration with {'verbose': True, 'output': 'log.txt'},
this method would return '--verbose --output log.txt'
"""
if not command.config:
return ""

Expand Down
25 changes: 25 additions & 0 deletions tests/test_commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ def test_build_groups():


def test_build_groups_with_template():
"""
Test the functionality of building command groups with a command template.
This test verifies that when a command is created with a template, the command's parameters
are correctly inherited from the specified template in the CLI manifest.
Parameters:
None
Raises:
AssertionError: If the command's parameters do not match the expected template parameters.
Example:
The test creates a CLI manifest with a command using a template that defines a parameter
with the name "name", and then checks that the command's parameters are correctly set.
"""
manifest = CLIManifest(
name="mycli",
help="",
Expand All @@ -79,6 +95,15 @@ def test_build_groups_with_template():


def test_build_groups_with_missing_template():
"""
Test the behavior of TyperCommander when initializing with a command referencing a non-existent template.
This test verifies that attempting to create a TyperCommander with a command that has a template
not defined in the command_templates dictionary raises a ValueError.
Raises:
ValueError: When a command references a template that does not exist in the manifest
"""
manifest = CLIManifest(
name="mycli",
help="",
Expand Down
Loading

0 comments on commit 2fdf3cb

Please sign in to comment.