Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor commander and Click support #49

Merged
merged 8 commits into from
Jan 24, 2025
Merged

Refactor commander and Click support #49

merged 8 commits into from
Jan 24, 2025

Conversation

jaykv
Copy link
Owner

@jaykv jaykv commented Jan 24, 2025

Summary by Sourcery

Implement CLI generation using Click.

New Features:

  • Generate CLI commands using the Click framework.

Tests:

  • Update tests to support Click generation.

Important

Refactor CLI generation to support both Click and Typer frameworks, with new examples and updated tests.

  • Behavior:
    • Implement CLI generation using Click framework in cliffy/commanders/click.py.
    • Add use_click option in CLIManifest to switch between Typer and Click.
    • Update generate_cli() in cliffy/commander.py to use ClickCommander or TyperCommander based on use_click.
  • Classes and Functions:
    • Add ClickCommander class in cliffy/commanders/click.py for Click-based CLI generation.
    • Refactor Commander class in cliffy/commander.py to support both Click and Typer.
    • Add ClickParser class in cliffy/commanders/click.py for parsing Click command parameters.
  • Examples and Tests:
    • Add examples/click_hello.yaml and examples/nested-cli.yaml for Click-based CLI examples.
    • Update tests/test_commander.py and tests/test_examples.py to test Click and Typer CLI generation.
  • Misc:
    • Update cliffy_schema.json to include use_click field.
    • Minor updates to Makefile for dependency management.

This description was created by Ellipsis for de2a003. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Added support for generating CLI using Click framework.
    • Introduced new configuration option to switch between Typer and Click.
    • Enhanced command group management with more structured approach.
    • New YAML configuration for a Click-based application named clickhello.
    • Added new command command4 under group2 in the nested CLI example.
  • Improvements

    • Refined command processing and group handling.
    • Updated CLI generation logic to support more complex command hierarchies.
    • Improved naming conventions for CLI scripts and paths.
    • Enhanced clarity and organization in command parsing and execution.
  • Tests

    • Expanded test cases to include new commands and validate command hierarchy.

Copy link

sourcery-ai bot commented Jan 24, 2025

Reviewer's Guide by Sourcery

This pull request introduces a new ClickCommander, which generates CLI applications using the Click framework. It also refactors the existing Commander class to support both Click and Typer frameworks. The changes include adding a ClickParser class to handle Click-specific parsing, updating the Commander class to manage groups and subcommands, and modifying the CLI generation process to accommodate both frameworks.

Class diagram showing the updated Commander hierarchy and new Click classes

classDiagram
    class Commander {
        <<abstract>>
        +generate_cli()
        +add_base_imports()*
        +add_base_cli()*
        +add_root_command()*
        +add_sub_command()*
        +add_main_block()*
        +define_groups()*
    }

    class ClickCommander {
        +add_base_imports()
        +add_base_cli()
        +add_root_command()
        +add_sub_command()
        +add_main_block()
        +define_groups()
    }

    class ClickParser {
        +parse_run_block()
        +parse_command_run()
        +get_param_names()
        +parse_params()
        +get_command_func_name()
        +get_parsed_command_name()
    }

    class BaseGroup {
        +name: str
        +short_name: str
        +parent_group: BaseGroup
        +commands: list[Command]
        +help: str
        +var_name()
        +is_root()
    }

    Commander <|-- ClickCommander
    ClickCommander --> ClickParser
    ClickCommander --> BaseGroup
Loading

File-Level Changes

Change Details Files
Added ClickCommander to support Click framework
  • Created ClickCommander class that inherits from Commander.
  • Implemented Click-specific CLI generation logic.
  • Added ClickParser class for parsing commands and parameters.
  • Added support for Click decorators and options.
  • Implemented alias support for Click commands.
  • Added support for Click groups and subcommands.
  • Added support for Click version and help options.
  • Added support for Click context settings.
  • Added support for Click main block.
cliffy/commanders/click.py
Refactored Commander class to support both Click and Typer
  • Refactored Group class to BaseGroup and added a Groups class to manage groups.
  • Added logic to handle command aliases using the
operator.
  • Added logic to merge command templates with individual commands.
  • Added logic to handle greedy commands.
  • Added logic to build group hierarchy from command names.
  • Added logic to process command templates and aliases.
  • Added abstract methods for framework-specific implementations.
  • Added logic to add subcommands to groups.
  • Added logic to add root commands.
  • Added logic to add lazy commands.
  • Added logic to check for greedy commands.
  • Added logic to convert greedy commands to lazy commands.
  • Added logic to merge command templates with individual commands.
  • Updated CLI generation process to accommodate both frameworks
    • Added logic to choose between ClickCommander and TyperCommander based on the manifest.
    • Updated CLI generation process to accommodate both frameworks.
    cliffy/transformer.py
    Updated tests to reflect changes in the Commander class
    • Updated tests to reflect changes in the Commander class.
    • Added tests for the new group hierarchy.
    • Added tests for the new alias logic.
    • Added tests for the new lazy command logic.
    tests/test_commander.py
    Updated TyperCommander to support group help
    • Updated TyperCommander to support group help.
    • Added logic to add group help to the Typer CLI.
    cliffy/commanders/typer.py
    Added example for Click framework
    • Added example for Click framework.
    examples/click_hello.yaml
    Updated manifest to support use_click flag
    • Added use_click flag to the manifest.
    cliffy/manifest.py

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!
    • Generate a plan of action for an issue: Comment @sourcery-ai plan on
      an issue to generate a plan of action for it.

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    Copy link
    Contributor

    coderabbitai bot commented Jan 24, 2025

    Walkthrough

    Arrr, matey! This pull request be bringin' a treasure trove of changes to the CLI framework! We've got a complete overhaul of the Commander class, introducin' a new BaseGroup and Groups class to manage command hierarchies. The code now supports more flexible group structures, with abstract methods in the Commander class that'll make subclasses dance to a new tune. A new ClickCommander joins the crew, and we've added a use_click flag to switch between Click and Typer frameworks!

    Changes

    File Changes
    cliffy/commander.py - Renamed Group to BaseGroup
    - Added new Groups class
    - Converted Commander to abstract base class
    cliffy/commanders/click.py - Added ClickParser class
    - Enhanced command parsing methods
    - Improved CLI generation logic
    cliffy/commanders/typer.py - Updated method signatures to use BaseGroup
    - Added define_groups method
    cliffy/manifest.py - Added use_click boolean field
    cliffy/transformer.py - Added import for ClickCommander
    - Dynamic commander class selection
    examples/ - Added click_hello.yaml
    - Updated nested-cli.yaml
    tests/ - Updated test cases for new group structures
    - Added tests for nested CLI
    Makefile - Updated generate-clis and generate-cleanup targets to include keyring
    examples/cliffy_schema.json - Added use_click property to the JSON schema

    Sequence Diagram

    sequenceDiagram
        participant Commander
        participant Groups
        participant BaseGroup
        participant Command
    
        Commander->>Groups: Initialize groups
        Groups->>BaseGroup: Create base groups
        BaseGroup-->>Groups: Register groups
        Commander->>Command: Add commands to groups
        Groups->>BaseGroup: Process commands
        BaseGroup-->>Commander: Confirm group structure
    
    Loading

    Possibly related PRs

    Poem

    Yo ho, CLI framework so bright! 🏴‍☠️
    Groups and commands take their flight
    Click or Typer, choose with might
    Nested structures, code so tight
    A pirate's code, pure delight! ⚓🌊

    ✨ Finishing Touches
    • 📝 Generate Docstrings (Beta)

    Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

    ❤️ Share
    🪧 Tips

    Chat

    There are 3 ways to chat with CodeRabbit:

    • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
      • I pushed a fix in commit <commit_id>, please review it.
      • Generate unit testing code for this file.
      • Open a follow-up GitHub issue for this discussion.
    • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
      • @coderabbitai generate unit testing code for this file.
      • @coderabbitai modularize this function.
    • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
      • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
      • @coderabbitai read src/utils.ts and generate unit testing code.
      • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
      • @coderabbitai help me debug CodeRabbit configuration file.

    Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

    CodeRabbit Commands (Invoked using PR comments)

    • @coderabbitai pause to pause the reviews on a PR.
    • @coderabbitai resume to resume the paused reviews.
    • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
    • @coderabbitai full review to do a full review from scratch and review all the files again.
    • @coderabbitai summary to regenerate the summary of the PR.
    • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
    • @coderabbitai resolve resolve all the CodeRabbit review comments.
    • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
    • @coderabbitai help to get help.

    Other keywords and placeholders

    • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
    • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
    • Add @coderabbitai anywhere in the PR title to generate the title automatically.

    CodeRabbit Configuration File (.coderabbit.yaml)

    • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
    • Please see the configuration documentation for more information.
    • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

    Documentation and Community

    • Visit our Documentation for detailed information on how to use CodeRabbit.
    • Join our Discord Community to get help, request features, and share feedback.
    • Follow us on X/Twitter for updates and announcements.

    Copy link

    @sourcery-ai sourcery-ai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Hey @jaykv - I've reviewed your changes and they look great!

    Here's what I looked at during the review
    • 🟡 General issues: 3 issues found
    • 🟢 Security: all looks good
    • 🟢 Testing: all looks good
    • 🟢 Complexity: all looks good
    • 🟢 Documentation: all looks good

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


    return "\n".join(decorators)

    def _parse_simple_param(self, param: SimpleCommandParam) -> str:
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    issue: Add error handling for malformed param_spec parsing

    The type_parts split operation could raise an IndexError if param_spec is malformed. Consider adding try/except handling and validation of the param_spec format.

    else:
    option_parts.append(f"default={param.default}")

    if param.help:
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    issue (bug_risk): Escape single quotes in help text

    Help text containing single quotes will cause syntax errors. Consider escaping single quotes in the help text.

    norm_script = script.to_script() if isinstance(script, RunBlockList) else script.root
    if not isinstance(norm_script, str):
    raise ValueError(f"Invalid script type: {type(norm_script)}")
    parsed_script: str = transform_bash(norm_script).strip()
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    issue: Add error handling for transform_bash failures

    Add try/except handling around transform_bash call to gracefully handle transformation failures.

    Comment on lines +183 to +186
    param_name = option_match.group(1).lstrip("-").replace("-", "_")
    params += f"{param_name}, "
    elif argument_match:
    param_name = argument_match.group(1).replace("-", "_")
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    issue (code-quality): Replace m.group(x) with m[x] for re.Match objects [×2] (use-getitem-for-re-match-groups)

    required = param_spec.endswith("!")

    # Determine if option or argument based on name prefix
    if name.startswith("-"):
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    issue (code-quality): We've found these issues:

    @coderabbitai coderabbitai bot changed the title @coderabbitai Refactor CLI framework with improved command management and Click support Jan 24, 2025
    @jaykv jaykv changed the title Refactor CLI framework with improved command management and Click support Refactor commander and Click support Jan 24, 2025
    Copy link

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ❌ Changes requested. Reviewed everything up to 8257e30 in 2 minutes and 17 seconds

    More details
    • Looked at 935 lines of code in 8 files
    • Skipped 0 files when reviewing.
    • Skipped posting 6 drafted comments based on config settings.
    1. cliffy/commander.py:37
    • Draft comment:
      Using a mutable default value for commands. Use default_factory=list to avoid shared state between instances.
    • Reason this comment was not posted:
      Decided after close inspection that this draft comment was likely wrong and/or not actionable:
      The BaseGroup class is new in this PR, so this is commenting on changed code. The issue is real and could cause bugs. However, since this is using Pydantic's BaseModel, the default list behavior may be different than standard Python classes. Looking at Pydantic docs, it actually handles mutable defaults correctly internally, so this isn't actually an issue.
      I should verify that Pydantic BaseModel definitely handles this correctly in all cases. There could be edge cases I'm not considering.
      Pydantic's documentation explicitly states that it handles mutable defaults safely by creating new instances. This is a core feature of Pydantic.
      The comment should be deleted because it raises a concern that isn't actually an issue when using Pydantic models, which handle mutable defaults correctly.
    2. cliffy/commander.py:52
    • Draft comment:
      Using a mutable default value for root. Use default_factory=dict to avoid shared state between instances.
    • Reason this comment was not posted:
      Marked as duplicate.
    3. cliffy/commander.py:69
    • Draft comment:
      Handle the case where full_path is an empty string to avoid unexpected behavior.
    • Reason this comment was not posted:
      Decided after close inspection that this draft comment was likely wrong and/or not actionable:
      The comment suggests adding explicit empty string handling, but this isn't necessary because: 1) The method is only called internally from process_command, 2) process_command only calls it when there's a "." in command.name, 3) The Pydantic model validation would catch empty names anyway. Adding explicit empty string handling would be redundant.
      I could be wrong about the validation - maybe there are edge cases where an empty string could slip through. And defensive programming is generally good practice.
      While defensive programming is good, in this case the validation is already handled by multiple layers - the command name validation, the "." checks, and the Pydantic model. Adding another check would be redundant.
      The comment should be deleted since empty string handling is already covered by existing validation and the method is only called in controlled internal contexts.
    4. cliffy/commander.py:71
    • Draft comment:
      Check if full_path is already in self.root before adding it to avoid overwriting existing groups.
    • Reason this comment was not posted:
      Marked as duplicate.
    5. cliffy/commander.py:70
    • Draft comment:
      Handle the case where full_path is an empty string to avoid unexpected behavior.
    • Reason this comment was not posted:
      Marked as duplicate.
    6. examples/click_hello.yaml:1
    • Draft comment:
      Use mkdocs material admonitions for better readability in documentation examples. For instance, use the following format for examples:
    !!! example "Title"
        This is an example of a mkdocs material admonitions. You can use code blocks here too.
    

    This applies to both click_hello.yaml and nested.yaml examples.

    • Reason this comment was not posted:
      Comment was on unchanged code.

    Workflow ID: wflow_AApUflOGlgWYf45V


    Want Ellipsis to fix these issues? Tag @ellipsis-dev in a comment. You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    cliffy/commander.py Outdated Show resolved Hide resolved
    Copy link
    Contributor

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 0

    🧹 Nitpick comments (3)
    cliffy/commanders/click.py (1)

    190-209: 'parse_params' method organizes the param decorators.

    Be mindful if Global Params might overshadow local ones in some edge scenario, but logic seems good.

    cliffy/commander.py (1)

    136-155: '_merge_command_template' merges commands with a template block.

    Arrr, take care of merges overshadowing manually set fields if that’s not intended.

    cliffy/commanders/typer.py (1)

    101-107: Hoist the sails for group definitions!
    'define_groups' method neatly arranges each group, though watch for multiline help in the future.

    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 97ede5b and 8257e30.

    📒 Files selected for processing (8)
    • cliffy/commander.py (6 hunks)
    • cliffy/commanders/click.py (1 hunks)
    • cliffy/commanders/typer.py (2 hunks)
    • cliffy/manifest.py (1 hunks)
    • cliffy/transformer.py (2 hunks)
    • examples/click_hello.yaml (1 hunks)
    • examples/nested.yaml (1 hunks)
    • tests/test_commander.py (3 hunks)
    ✅ Files skipped from review due to trivial changes (1)
    • examples/nested.yaml
    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: CodeBeaver AI
    🔇 Additional comments (74)
    cliffy/commanders/click.py (28)

    2-2: Arrr, no concerns here, matey!

    Ye be importin' the Union type from typing, which be perfectly fine.


    5-5: Ahoy, 'Commander' and 'BaseGroup' import be a neat approach!

    Keep a weather eye out for consistent usage with your newly minted classes.


    9-10: Steerin’ the script transformation to safe waters!

    Ye be importin' transform_bash; all seems well, but be mindful of performance if scripts grow large.


    11-18: Avast! The 'CLIManifest' and param classes be loaded.

    All these imports be fair and square. Just be sure none be left adrift if unused.


    21-21: A new 'ClickCommander' class, me hearty!

    This be the anchor of the new approach, no immediate issues spotted.


    23-23: Docstring be clear as the horizon on a calm sea!

    Nice clarity mentionin' the usage of Click.


    25-30: Constructor sets sail with 'ClickParser' and base imports!

    All looks like plain sailing. The usage of rich_click is interesting—just mind licensing if it’s external.


    33-36: Arrr, your 'add_base_imports' sets a sweet introduction.

    The dynamic injection of imports be well done.


    37-58: 'add_base_cli' method hoists the mainsail with grouped context settings!

    Well structured and prepared for optional aliases. Good usage of an exit after showin’ aliases.


    60-69: Click group definition gets a well-fitting coat of paint.

    All be spick and span. Mind you confirm the '--aliases' option doesn't conflict with user-submitted options.


    76-79: The 'define_groups' method outlines your group creation.

    Clear and purposeful, me hearty.


    80-85: Conditional skip for root group checks, well done!

    Ye mark root groups to avoid accidental reuse. The help text replacement be short but sweet.


    89-90: 'add_group' gives each group a place on the deck.

    Linkin' parent group gracefully.


    92-92: Small addition to the CLI string.

    No trouble spotted.


    95-95: 'add_root_command' sets the stage for commands.

    No immediate concerns, matey.


    96-99: Guard check for run block.

    If no run block, we skip. Fair enough—keeps the ship uncluttered.


    100-109: Root commands capturing aliases, arrr!

    This be a robust approach to handle multiple sets of commands and their aliases.


    111-119: Alias creation in root commands.

    Bundling them with hidden attribute is a cunning plan.


    120-120: Sub-command aggregator for group.

    A single line that anchors the method. Looks well set.


    121-123: Naming and help for commands.

    All be consistent with the rest o’ the code.


    126-131: Sub-command method’s final flourish.

    Neatly sets the command run block. No issues, me hearty.


    133-139: Alias for sub-commands, hidden from the main listing.

    Clever trick, well used.


    142-143: Addin' the main block to the CLI.

    'if name == "main":' suits a direct execution.


    144-146: Invoke the 'cli' group.

    A neat piece for direct script usage.


    149-188: 'ClickParser' constructor, parse_run_block, parse_command_run—ye do be handling script transformations thoroughly.

    All be logically cohesive. Just watch for potential corner cases if script gets monstrous.


    210-252: '_parse_simple_param' yields clarity for arguments vs. options.

    Mind the distinction between required vs. default—some combos can cause confusion if both exist.


    253-276: '_build_click_option' robustly handles short flags, types, defaults, and help.

    Spot on, me hearty!


    278-289: 'get_command_func_name', 'get_parsed_command_name', and 'to_args' keep naming in shipshape.

    They appear to handle edge cases adequately.

    cliffy/commander.py (25)

    3-4: Marking 'Commander' as an ABC: Shiver me timbers, a bold move!

    This clarifies it be an abstract base class.


    7-7: Importing 'defaultdict'—smart to handle grouped lists or dictionaries, matey.

    Just be certain not to introduce circular references.


    23-23: Sizable import block, but the usage is quite cohesive.

    No further suggestions.


    33-33: 'BaseGroup' class sets the foundation for the new group structure.

    A good pivot from older group classes, me hearty.


    35-37: Short name, optional parent group, and list of commands—yo-ho-ho, a flexible group design.

    Ensure no cyclical parent references creep in.


    40-47: 'var_name' returns a valid Python variable, 'is_root' checks the anchor.

    Nifty approach to keep group naming consistent.


    49-52: 'Groups' container easing iteration and retrieval.

    Surely better than a plain dictionary.


    53-68: Iterator, items, values, indexing, length: all hands on deck for grouping convenience.

    All typical dictionary-like expansions. Approved.


    69-90: Adding groups by full path, bravely forging parent group references.

    Watch for missing sub-groups if multiple dots appear. Looks well-structured though.


    91-99: 'process_command' ensures commands find their rightful group.

    No issues be spotted.


    100-100: Commander class decorated with ABC docstring.

    Aye, no trouble.


    113-113: Double definition of 'groups' in slots might be redundant.

    Check if 'groups' is repeated by oversight. Could lead to confusion.


    121-122: Initializing 'groups' as 'Groups()'—the new aggregator.

    Central place for group logic. All good.


    126-126: Assigning default names for unnamed commands.

    This autopopulation is a handy fallback.


    127-129: 'aliases_by_commands' is a defaultdict of lists.

    Neat approach. Keep an eye that no extraneous aliases slip in.


    158-158: 'build_groups' sets the skeleton.

    No issues, me hearty.


    164-165: Merging user-supplied template on each command.

    Practical approach.


    166-171: Handling '|' in command name for aliases.

    Nice trick. Keep your doc references updated so others know.
    [approve]


    172-180: Finish group building by checking if a command is a script-less group.

    Sets group help if found. Crisp logic.


    181-187: 'add_subcommands' loops through groups, skip root, add group.

    Then subcommands follow. Very clean.


    202-202: 'add_root_commands': roots get their rightful place.

    Sensible design.


    239-239: 'add_command' for nested commands: a direct approach.

    No scurvy to be found.


    246-248: 'add_greedy_commands' for placeholders like '(*)'.

    Clever tactic for multi-group expansions.


    251-258: 'add_lazy_command': the real wizard behind the scenes.

    Bravely manipulates placeholders in commands, keep watch for performance if many placeholders exist.


    323-350: Abstract methods for each step: define_groups, add_group, add_base_imports, etc.

    This sets up an interface for future expansions.

    cliffy/transformer.py (2)

    10-10: Import of 'ClickCommander' gives us a new route for generation.

    No red flags, me hearty.


    47-48: Dynamic commander class selection with 'use_click' field.

    Brilliant design choice—lets us swap frameworks swiftly.

    cliffy/commanders/typer.py (8)

    3-3: Arr, the imports be lookin' shipshape!
    No conflicts or naming woes are spotted after hooking in BaseGroup.


    87-87: A fine tweak to the help text, me hearty!
    Using empty_or_help for conditionally showin' help text is clear as crystal.


    93-93: Command registration sets sail smoothly!
    No hidden reefs found in cli.command usage.


    110-114: Smart group addition, matey!
    Proper nesting logic with parent_group is well-anchored.


    116-117: Sub-command docstring be quite helpful!
    The purpose and usage are plain to see.


    122-123: Conditionally attachin' help text be wise.
    This approach stays consistent with the root commands.


    128-128: Command creation sails forth!
    All parameters match the new approach.


    133-133: Alias commands be set with no mutiny!
    No scurvy found; aliases be hidden properly.

    tests/test_commander.py (7)

    65-69: Aye, group additions be tested!
    Verifications of group1 and group2 commands are well-labeled.


    73-73: Avast, the group count check be correct!
    Ensures the manifest translation forms the correct number of groups.


    76-77: Aar, help text tested for clarity!
    Properly ensures each group’s help is captured.


    153-155: Param expansions run without sinking!
    The assertion for replaced placeholders appears accurate.


    169-170: Command retrieval by iteration is sound!
    Fetching commands from the group dictionary is neat and tidy.


    181-183: Groups be shaped for a complex voyage!
    Introducin' group1 and group2 with clear help messages.


    190-197: Detailed checks for nested subgroups, arr!
    The hierarchical logic and help text validations be thorough.

    cliffy/manifest.py (1)

    253-253: use_click hoists a new flag, me hearties!
    Now the CLI can set sail with Click or Typer.

    examples/click_hello.yaml (3)

    1-3: This new manifest be stowin’ a Click-based CLI, argh!
    name, version, and use_click options anchor the plan for a clickable CLI.


    5-7: Help message and commands starboard!
    Defining a simple 'hello' command with an alias shows promise for a friendly interface.


    9-9: Empty line—no troubles, matey!
    This empty space does not hide any cursed code.

    Copy link

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    👍 Looks good to me! Incremental review on 67fbc5c in 29 seconds

    More details
    • Looked at 63 lines of code in 3 files
    • Skipped 0 files when reviewing.
    • Skipped posting 3 drafted comments based on config settings.
    1. cliffy/loader.py:38
    • Draft comment:
      Ensure replace('-', '_') is consistently applied across the codebase for CLI names to avoid potential import errors. This is already done here and in get_cli_script, but verify other parts of the codebase.
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The code in cliffy/loader.py uses replace('-', '_') in multiple places to ensure CLI names are valid Python identifiers. This is a good practice, but it should be consistently applied across the codebase.
    2. tests/test_examples.py:51
    • Draft comment:
      The test cases for nested-cli are well-defined and cover the expected outputs for the commands. Ensure that any future changes to the command structure are reflected in these tests.
    • Reason this comment was not posted:
      Confidence changes required: 20%
      The test file tests/test_examples.py includes a test for nested-cli, which is a new feature. The test cases seem to cover the expected outputs for the commands defined in examples/nested-cli.yaml. This is a good practice to ensure new features are tested.
    3. examples/nested-cli.yaml:1
    • Draft comment:
      Consider using mkdocs material admonitions for examples to improve readability. For instance, use the following format:
    !!! example "Nested CLI Example"
        name: nested-cli
        version: 0.1.0
        help: Demo nested command structure
        commands:
          group1.subgroup.command1: $ echo "hello"
          group1.subgroup.command2: $ echo "world"
          group2.command3: $ echo "foo"
    • Reason this comment was not posted:
      Comment was on unchanged code.

    Workflow ID: wflow_npYoxnMHe9zA7FnU


    You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link
    Contributor

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 0

    🧹 Nitpick comments (1)
    tests/test_examples.py (1)

    51-55: Shiver me timbers, yer nested subcommand tests be grand!

    The coverage for multiple subcommands be impressive. Consider testin' unrecognized commands or error-handling flows to keep mutiny in check.

    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 8257e30 and 67fbc5c.

    📒 Files selected for processing (3)
    • cliffy/loader.py (2 hunks)
    • examples/nested-cli.yaml (1 hunks)
    • tests/test_examples.py (2 hunks)
    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: CodeBeaver AI
    🔇 Additional comments (5)
    cliffy/loader.py (2)

    38-38: Arr, starboard rename be the right course, me hearty!

    Changin' hyphens to underscores keeps Python module scurvy at bay. Tidy work, mate!


    48-48: Ahoy, consistency in yer import path is shipshape!

    Ye be replacin' hyphens with underscores in both file path and import statement, maintainin' harmony across the deck, argh!

    tests/test_examples.py (1)

    21-22: Avast! Ye've added nested-cli to yer builds, mate.

    This new entry helps test the nested structure feature thoroughly. Set sail with confidence!

    examples/nested-cli.yaml (2)

    3-3: Arr, the new name 'nested-cli' be on point!

    It aligns well with yer hyphen-to-underscore approach in the loader, savvy?


    5-5: By the powers! This 'help' field adds clarity, matey!

    Settin' up user-friendly info fosters smoother sailin' for all hands on deck.

    Copy link

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    👍 Looks good to me! Incremental review on 5c5c0a5 in 19 seconds

    More details
    • Looked at 12 lines of code in 1 files
    • Skipped 0 files when reviewing.
    • Skipped posting 1 drafted comments based on config settings.
    1. cliffy/commander.py:113
    • Draft comment:
      The 'groups' attribute is initialized in the constructor but removed from slots. Ensure consistency between slots and attributes used in the class.
    • Reason this comment was not posted:
      Comment did not seem useful.

    Workflow ID: wflow_waI9jqMmaulA9ojo


    You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link

    @ellipsis-dev ellipsis-dev bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    👍 Looks good to me! Incremental review on de2a003 in 58 seconds

    More details
    • Looked at 847 lines of code in 22 files
    • Skipped 0 files when reviewing.
    • Skipped posting 6 drafted comments based on config settings.
    1. Makefile:42
    • Draft comment:
      Ensure that keyring is necessary for CLI generation. If not, consider removing it from the generate-clis and generate-cleanup targets to avoid unnecessary dependencies.
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The generate-clis and generate-cleanup targets in the Makefile now include keyring in the list of packages to install and uninstall. This is a good practice to ensure that all dependencies are managed properly. However, it's important to ensure that keyring is actually used in the CLI generation process, otherwise, it might be an unnecessary dependency.
    2. cliffy/commander.py:88
    • Draft comment:
      Ensure that add_command_to_group correctly handles commands without a dot in their name, as these should be added to the root group instead of creating a new group.
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The add_command_to_group method in the Groups class assumes that the command name will always contain a dot to determine the group name. This might not be the case for root commands, which could lead to incorrect group assignments.
    3. cliffy/commander.py:197
    • Draft comment:
      Good use of self.root_group.commands to handle root commands separately. This keeps the logic clear and organized.
    • Reason this comment was not posted:
      Confidence changes required: 0%
      The add_root_commands method in the Commander class iterates over self.root_group.commands to add root commands. This is a clear and efficient way to handle root commands separately from grouped commands.
    4. cliffy/commanders/click.py:77
    • Draft comment:
      Ensure that all groups are correctly initialized and populated before calling define_groups to avoid potential issues with undefined groups.
    • Reason this comment was not posted:
      Confidence changes required: 50%
      The define_groups method in ClickCommander iterates over self.groups.values() to define groups. This is a straightforward approach, but it assumes that all groups have been correctly initialized and populated.
    5. cliffy/manifest.py:253
    • Draft comment:
      Good use of default values for fields in CLIManifest to ensure they are always initialized.
    • Reason this comment was not posted:
      Confidence changes required: 0%
      The use_click field in CLIManifest has been updated to use a default value. This is a good practice to ensure that the field always has a value, even if not explicitly set by the user.
    6. examples/generated/aiprompt.py:1
    • Draft comment:
      Use mkdocs material admonitions for examples. For instance, use the following format for examples:
    !!! example "Generated aiprompt"
        This is an example of a mkdocs material admonitions. You can use code blocks here too.
    
    • Reason this comment was not posted:
      Comment was on unchanged code.

    Workflow ID: wflow_tuyEkJ1Monz8rk8Q


    You can customize Ellipsis with 👍 / 👎 feedback, review rules, user-specific overrides, quiet mode, and more.

    Copy link
    Contributor

    @coderabbitai coderabbitai bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 2

    🧹 Nitpick comments (3)
    examples/nested-cli.yaml (1)

    10-10: Arrr! Missing newline at the end of the file, ye scallywag!

    Add a newline character at the end of the file to comply with YAML best practices.

       command4: $ echo "bar"
    +
    🧰 Tools
    🪛 yamllint (1.35.1)

    [error] 10-10: no new line character at the end of file

    (new-line-at-end-of-file)

    Makefile (1)

    42-42: Shiver me timbers! The keyring package be needin' a version pin!

    All other packages have their versions pinned. For consistency and to prevent future compatibility issues, consider pinning the keyring package version.

    -pip install requests "six<1.0.0" rich keyring
    +pip install requests "six<1.0.0" rich "keyring==24.3.0"
    examples/cliffy_schema.json (1)

    300-305: Aye! Add an example for use_click property, ye landlubber!

    Ye schema be needin' an example to show how to use the Click framework, matey!

             "use_click": {
                 "default": false,
                 "description": "Generate CLI to Click instead of Typer",
                 "title": "Use Click",
    -            "type": "boolean"
    +            "type": "boolean",
    +            "examples": [
    +                {
    +                    "use_click": true,
    +                    "name": "my-cli",
    +                    "version": "1.0.0"
    +                }
    +            ]
             },
    📜 Review details

    Configuration used: CodeRabbit UI
    Review profile: CHILL
    Plan: Pro

    📥 Commits

    Reviewing files that changed from the base of the PR and between 67fbc5c and de2a003.

    ⛔ Files ignored due to path filters (13)
    • examples/generated/aiprompt.py is excluded by !**/generated/**
    • examples/generated/clickhello.py is excluded by !**/generated/**
    • examples/generated/db.py is excluded by !**/generated/**
    • examples/generated/environ.py is excluded by !**/generated/**
    • examples/generated/hello.py is excluded by !**/generated/**
    • examples/generated/nested_cli.py is excluded by !**/generated/**
    • examples/generated/penv.py is excluded by !**/generated/**
    • examples/generated/pydev.py is excluded by !**/generated/**
    • examples/generated/requires.py is excluded by !**/generated/**
    • examples/generated/taskmaster.py is excluded by !**/generated/**
    • examples/generated/template.py is excluded by !**/generated/**
    • examples/generated/todo.py is excluded by !**/generated/**
    • examples/generated/town.py is excluded by !**/generated/**
    📒 Files selected for processing (9)
    • Makefile (1 hunks)
    • cliffy/commander.py (6 hunks)
    • cliffy/commanders/click.py (1 hunks)
    • cliffy/commanders/typer.py (2 hunks)
    • cliffy/manifest.py (1 hunks)
    • examples/cliffy_schema.json (1 hunks)
    • examples/nested-cli.yaml (1 hunks)
    • tests/test_commander.py (3 hunks)
    • tests/test_examples.py (2 hunks)
    🚧 Files skipped from review as they are similar to previous changes (1)
    • cliffy/manifest.py
    🧰 Additional context used
    🪛 yamllint (1.35.1)
    examples/nested-cli.yaml

    [error] 10-10: no new line character at the end of file

    (new-line-at-end-of-file)

    ⏰ Context from checks skipped due to timeout of 90000ms (1)
    • GitHub Check: CodeBeaver AI
    🔇 Additional comments (12)
    examples/nested-cli.yaml (1)

    3-5: Ahoy! Nice and clear command structure, matey!

    Ye've done a fine job with the command structure and the help text be crystal clear!

    cliffy/commanders/typer.py (3)

    87-88: Aye! Smart move with the help text handling, ye code wizard!

    The empty_or_help variable be making the code more DRY and easier to maintain. Well done!

    Also applies to: 120-121


    101-105: Arrr! A fine addition with the define_groups method!

    The new method provides a clean way to handle group definitions. The implementation be ship-shape!


    108-112: Ye've done well with the group nesting, matey!

    The updated add_group method with proper parent-child relationships makes the command hierarchy crystal clear.

    tests/test_examples.py (1)

    51-56: Yo ho ho! Excellent test coverage for the nested commands!

    The test cases be covering all the commands in the nested structure, including the new command4. That's the way to prevent bugs from sneaking aboard!

    tests/test_commander.py (4)

    65-69: Aye! The group structure be well-organized!

    The command hierarchy with help text makes the structure crystal clear. Good job, matey!


    153-155: Smart move with the type hints, ye code sailor!

    Adding type hints to suppress warnings makes the code more maintainable and type-safe.


    169-170: Arrr! A more Pythonic approach to command iteration!

    Using next(iter()) instead of list comprehension be more efficient and cleaner.


    181-197: Shiver me timbers! That's some fine group hierarchy testing!

    The test coverage for complex command hierarchies be thorough and well-structured. The parent-child relationships and help text verification be spot on!

    cliffy/commanders/click.py (3)

    155-155: Arrr! Add error handling for transform_bash, ye scurvy dog!

    Ye be needin' to wrap that transform_bash call in a try/except block, lest ye hit rough seas!

    -        parsed_script: str = transform_bash(norm_script).strip()
    +        try:
    +            parsed_script: str = transform_bash(norm_script).strip()
    +        except Exception as e:
    +            raise ValueError(f"Failed to transform bash script: {e}") from e

    270-271: Shiver me timbers! Escape them single quotes in help text!

    Ye help text be needin' some escapin' of single quotes, or ye'll be walkin' the plank with syntax errors!

    -            option_parts.append(f"help='{param.help}'")
    +            option_parts.append(f"help='{param.help.replace(\"'\", \"\\'\")}'")

    208-217: Avast! Secure yer param_spec parsing, matey!

    Ye type_parts split operation could raise an IndexError if param_spec be malformed. Add some safety checks!

    -        type_parts = param_spec.split("=")
    -        param_type = type_parts[0].rstrip("!")
    -        default_value = type_parts[1] if len(type_parts) > 1 else None
    +        try:
    +            type_parts = param_spec.split("=")
    +            param_type = type_parts[0].rstrip("!")
    +            default_value = type_parts[1] if len(type_parts) > 1 else None
    +        except IndexError:
    +            raise ValueError(f"Invalid param_spec format: {param_spec}")

    Comment on lines +135 to +137
    template = self.manifest.command_templates.get(command.template)
    if not template:
    raise ValueError(f"Template {command.template} undefined in command_templates")
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Arrr! Add more validation for ye template existence!

    Ye be needin' to validate the template name before tryin' to fetch it, or ye might hit a reef!

    +        if not command.template.strip():
    +            raise ValueError("Empty template name provided")
    +
             template = self.manifest.command_templates.get(command.template)
             if not template:
    -            raise ValueError(f"Template {command.template} undefined in command_templates")
    +            raise ValueError(
    +                f"Template '{command.template}' not found in command_templates. "
    +                f"Available templates: {list(self.manifest.command_templates.keys())}"
    +            )
    📝 Committable suggestion

    ‼️ IMPORTANT
    Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    Suggested change
    template = self.manifest.command_templates.get(command.template)
    if not template:
    raise ValueError(f"Template {command.template} undefined in command_templates")
    if not command.template.strip():
    raise ValueError("Empty template name provided")
    template = self.manifest.command_templates.get(command.template)
    if not template:
    raise ValueError(
    f"Template '{command.template}' not found in command_templates. "
    f"Available templates: {list(self.manifest.command_templates.keys())}"
    )

    Comment on lines +66 to +90
    def add_group_by_full_path(self, full_path: str) -> str:
    if "." not in full_path:
    self.root[full_path] = BaseGroup(name=full_path, short_name=full_path, parent_group=None)
    return full_path

    group_name, _ = full_path.rsplit(".", 1)
    if "." in group_name:
    parent_group_name, short_name = group_name.rsplit(".", 1)
    if parent_group_name in self.root:
    parent_group = self.root[parent_group_name]
    else:
    parent_group = self.root[self.add_group_by_full_path(parent_group_name)]
    else:
    short_name = group_name
    parent_group = None

    if group_name in self.root:
    return group_name

    self.root[group_name] = BaseGroup(name=group_name, short_name=short_name, parent_group=parent_group)
    return group_name

    def add_command_to_group(self, command: Command) -> None:
    group_name = self.add_group_by_full_path(command.name)
    self.root[group_name].commands.append(command)
    Copy link
    Contributor

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Ye need better error handling in add_group_by_full_path, cap'n!

    Add some validation to prevent invalid group paths from sinkin' yer ship!

         def add_group_by_full_path(self, full_path: str) -> str:
    +        if not full_path or not isinstance(full_path, str):
    +            raise ValueError(f"Invalid group path: {full_path}")
    +
             if "." not in full_path:
                 self.root[full_path] = BaseGroup(name=full_path, short_name=full_path, parent_group=None)
                 return full_path
    📝 Committable suggestion

    ‼️ IMPORTANT
    Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    Suggested change
    def add_group_by_full_path(self, full_path: str) -> str:
    if "." not in full_path:
    self.root[full_path] = BaseGroup(name=full_path, short_name=full_path, parent_group=None)
    return full_path
    group_name, _ = full_path.rsplit(".", 1)
    if "." in group_name:
    parent_group_name, short_name = group_name.rsplit(".", 1)
    if parent_group_name in self.root:
    parent_group = self.root[parent_group_name]
    else:
    parent_group = self.root[self.add_group_by_full_path(parent_group_name)]
    else:
    short_name = group_name
    parent_group = None
    if group_name in self.root:
    return group_name
    self.root[group_name] = BaseGroup(name=group_name, short_name=short_name, parent_group=parent_group)
    return group_name
    def add_command_to_group(self, command: Command) -> None:
    group_name = self.add_group_by_full_path(command.name)
    self.root[group_name].commands.append(command)
    def add_group_by_full_path(self, full_path: str) -> str:
    if not full_path or not isinstance(full_path, str):
    raise ValueError(f"Invalid group path: {full_path}")
    if "." not in full_path:
    self.root[full_path] = BaseGroup(name=full_path, short_name=full_path, parent_group=None)
    return full_path
    group_name, _ = full_path.rsplit(".", 1)
    if "." in group_name:
    parent_group_name, short_name = group_name.rsplit(".", 1)
    if parent_group_name in self.root:
    parent_group = self.root[parent_group_name]
    else:
    parent_group = self.root[self.add_group_by_full_path(parent_group_name)]
    else:
    short_name = group_name
    parent_group = None
    if group_name in self.root:
    return group_name
    self.root[group_name] = BaseGroup(name=group_name, short_name=short_name, parent_group=parent_group)
    return group_name
    def add_command_to_group(self, command: Command) -> None:
    group_name = self.add_group_by_full_path(command.name)
    self.root[group_name].commands.append(command)

    @jaykv jaykv merged commit ee5c5dd into main Jan 24, 2025
    6 of 7 checks passed
    @jaykv jaykv deleted the refactor-group-1 branch January 24, 2025 18:30
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    None yet
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    1 participant