Skip to content

Commit

Permalink
refactor and bug fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jaykv committed Jan 16, 2025
1 parent d0cef7a commit 14acffc
Show file tree
Hide file tree
Showing 22 changed files with 524 additions and 178 deletions.
8 changes: 5 additions & 3 deletions cliffy/commander.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ def add_vars(self) -> None:
def add_functions(self) -> None:
if not self.manifest.functions:
return

for func in self.manifest.functions:
self.cli += f"{transform_bash(func)}\n"
if isinstance(self.manifest.functions, str):
self.cli += self.manifest.functions + "\n"
elif isinstance(self.manifest.functions, list):
for func in self.manifest.functions:
self.cli += f"{transform_bash(func)}\n"
self.cli += "\n"

def add_command(self, command: Command) -> None:
Expand Down
52 changes: 39 additions & 13 deletions cliffy/manifest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from functools import cached_property
from typing import Any, ItemsView, Iterator, Optional, Union
from pydantic import BaseModel, Field, RootModel, field_validator, ValidationInfo
from .helper import wrap_as_comment
Expand Down Expand Up @@ -28,6 +29,42 @@ def __iter__(self) -> Iterator[str]: # type: ignore[override]
def items(self) -> ItemsView[str, str]: # type: ignore[override]
return self.root.items()

@cached_property
def raw_name(self) -> str:
return next(iter(self.root)).strip()

@property
def raw_type(self) -> str:
return self.root[self.raw_name]

@property
def name(self) -> str:
return self.raw_name.split("|")[0] if "|" in self.raw_name else self.raw_name

@property
def type(self) -> str:
parsed_type = self.raw_type.split("=")[0].strip() if "=" in self.raw_type else self.raw_type
return parsed_type.rstrip("!")

@property
def required(self) -> bool:
return self.raw_type.endswith("!")

@property
def default(self) -> Optional[str]:
return self.raw_type.split("=")[1].strip() if "=" in self.raw_type else None

@property
def short(self) -> Optional[str]:
return self.raw_name.split("|")[1] if "|" in self.raw_name else None

def is_option(self) -> bool:
return self.raw_name.startswith("-")

@property
def help(self) -> str:
return ""


class CommandParam(BaseModel):
"""
Expand Down Expand Up @@ -66,17 +103,6 @@ def short_only_with_option(cls, v: str, info: ValidationInfo) -> str:
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 @@ -248,9 +274,9 @@ class CLIManifest(BaseModel):
description="String block or list of strings containing any module imports. "
"These can be used to import any python modules that the CLI depends on.",
)
functions: list[str] = Field(
functions: Union[str, list[str]] = Field(
default=[],
description="List of helper function definitions. "
description="String block or list of helper function definitions. "
"These functions should be defined as strings that can be executed by the Python interpreter.",
)

Expand Down
129 changes: 35 additions & 94 deletions cliffy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ def build_param_type(
)
"""
parsed_param_type = f"{param_name}: {param_type} = typer.{typer_cls}"
if not default_val:
if default_val is None:
# Required param needs ...
parsed_param_type += "(..." if is_required else "(None"
else:
parsed_param_type += f"({default_val.strip()}"
parsed_param_type += f"({default_val}"

if aliases and typer_cls == "Option":
if param_name.startswith("--"):
Expand All @@ -120,113 +120,51 @@ def build_param_type(
parsed_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.
Args:
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"
arg_aliases: list[str] = []
def parse_param(self, param: Union[CommandParam, GenericCommandParam, SimpleCommandParam]) -> str:
if isinstance(param, GenericCommandParam):
return f"{param.root}, "

# extract default val before parsing it
if "=" in param_type:
param_type = param_type.split("=")[0].strip()
typer_cls = "Option" if param.is_option() else "Argument"
norm_param_name = self.normalize_param_name(param.name)
param_type = param.type

# lstrip - before parsing it
if self.is_param_option(param_name):
param_name, arg_aliases = self.capture_param_aliases(param_name)
if isinstance(param, SimpleCommandParam) and "typer." in param.raw_type:
return f"{norm_param_name.strip()}: {param_type.strip()}, "

# rstrip ! before parsing it
if is_required:
param_type = param_type[:-1]
if param_type in self.manifest.types:
return f"{norm_param_name}: {self.manifest.types[param_type]},"

# replace - with _ for arg name
param_name = param_name.replace("-", "_")
aliases = [param.short] if param.short else None

# check for a type def that matches param_type
if param_type in self.manifest.types:
return f"{param_name}: {self.manifest.types[param_type]},"
if isinstance(param, CommandParam):
# wrap default_val in quotes if it's a string and not wrapped already
default_val = (
f'"{param.default.replace('"', r"\"")}"'
if param.type == "str" and param.default is not None
else param.default
)
else:
# for simple, assume it's already wrapped
default_val = param.default

return self.build_param_type(
param_name,
param_type,
typer_cls=param_typer_cls,
aliases=arg_aliases,
param_name=norm_param_name,
param_type=param_type,
typer_cls=typer_cls,
aliases=aliases,
default_val=default_val,
is_required=is_required,
is_required=param.required,
help=param.help,
)

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.
Args:
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 ""

parsed_command_params = ""
combined_command_params = self.manifest.global_params + command.params
for param in combined_command_params:
if isinstance(param, CommandParam):
aliases = [param.short] if param.short else None

parsed_command_params += (
self.build_param_type(
param_name=param.name,
param_type=param.type,
typer_cls="Option" if param.is_option() else "Argument",
help=param.help,
aliases=aliases,
default_val=str(param.default) if param.default is not None else None,
is_required=param.required,
)
+ " "
)
elif isinstance(param, GenericCommandParam):
parsed_command_params += f"{param}, "
elif isinstance(param, SimpleCommandParam):
param_name, param_type = next(iter(param.root.items()))

if "typer." in param_type:
parsed_command_params += f"{param_name.strip()}: {param_type.strip()}, "
else:
parsed_command_params += f"{self.parse_param(param_name.strip(), param_type.strip())} "

# strip the extra ", "
return parsed_command_params[:-2]
parsed_command_params = "".join(f"{self.parse_param(param)} " for param in combined_command_params)
# strip the extra ", " if exists
return parsed_command_params.strip().rstrip(",")

def get_parsed_config(self, command: Command) -> str:
"""
Expand All @@ -251,6 +189,9 @@ def get_parsed_config(self, command: Command) -> str:
configured_options = command.config.model_dump(exclude_unset=True)
return self.to_args(configured_options)

def normalize_param_name(self, name: str) -> str:
return name.lstrip("-").replace("-", "_")

def get_command_func_name(self, command: Command) -> str:
"""a -> a, a.b -> a_b, a-b -> a_b, a|b -> a_b"""
if not command.name.replace(".", "").replace("-", "").replace("_", "").replace("|", "").isalnum():
Expand Down
Loading

0 comments on commit 14acffc

Please sign in to comment.