Skip to content

[codex] address PR review comments#4

Merged
rolling-codes merged 1 commit into
mainfrom
codex-easycord-refactor-features
Apr 22, 2026
Merged

[codex] address PR review comments#4
rolling-codes merged 1 commit into
mainfrom
codex-easycord-refactor-features

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Apr 22, 2026

Copy link
Copy Markdown
Owner

What changed

  • Fixed the grouped bot example runtime bug by calling run_bot(build_bot()) with the correct signature.
  • Aligned the docs with the bulk plugin/group APIs and the current beginner-facing examples.
  • Hardened the shared plugin helpers and bundled plugins against malformed data and unsafe exception swallowing.
  • Expanded test coverage for the new metadata forwarding and bulk registration paths.

Why

These changes close out the review feedback from the refactor PR and make the follow-up cleanup safer and easier to use.

Validation

  • python -m compileall .\easycord .\examples .\docs .\tests
  • python -m pytest .\tests\test_bot.py .\tests\test_group.py .\tests\test_composer.py .\tests\test_server_commands.py .\tests\test_package_exports.py .\tests\test_decorators.py

Notes

  • The Windows pytest temp/cache permission warning still appears in this environment, but the focused test slice passes.

Summary by Sourcery

Tighten plugin safety and metadata handling while aligning examples, docs, and tests with the new bulk APIs and runtime helpers.

Bug Fixes:

  • Fix the grouped bot example to call the shared runtime helper with the correct signature.

Enhancements:

  • Harden plugin config and template handling against malformed JSON and invalid placeholders, with clearer error messaging instead of silent failures.
  • Restrict level rank rendering to numeric keys and type-hint internal helpers to better signal expected inputs and outputs.
  • Replace blanket exception swallowing in moderation welcome DMs with granular error handling and logging, ensuring unexpected errors propagate.

Documentation:

  • Update getting-started, fork-and-expand, API, and release-notes docs to showcase bulk plugin/group loading, composer group support, and current example usage patterns.

Tests:

  • Extend bot, server command, and composer tests to cover metadata forwarding (contexts/installs), default plugin bulk loading, and composer group accumulation.
  • Add a package marker for the examples directory so imports from examples resolve consistently across environments.

Chores:

  • Clarify server_commands public API docs to include the default plugin loader helper.

@sourcery-ai

sourcery-ai Bot commented Apr 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Aligns examples and docs with the new bulk plugin/group APIs, hardens shared plugin utilities and bundled plugins against malformed data and unsafe exception swallowing, fixes the grouped bot runtime entrypoint, and expands tests to cover metadata forwarding, group composition, and bulk plugin loading behavior.

Sequence diagram for welcome message template validation

sequenceDiagram
    actor Admin
    participant DiscordClient
    participant WelcomePlugin
    participant TemplateFormatter as format_template

    Admin->>DiscordClient: Invoke /set_welcome_message message="Welcome {user} to {server}!"
    DiscordClient->>WelcomePlugin: set_welcome_message(ctx, message)
    WelcomePlugin->>WelcomePlugin: guild = ctx.guild
    alt No guild (DM)
        WelcomePlugin-->>DiscordClient: ctx.respond("This command only works in a server.")
        DiscordClient-->>Admin: Ephemeral error message
    else In guild
        WelcomePlugin->>TemplateFormatter: format_template(message, user=ctx.user.mention, server=guild.name)
        alt Template invalid
            TemplateFormatter-->>WelcomePlugin: raise KeyError or ValueError
            WelcomePlugin-->>DiscordClient: ctx.respond("Invalid template. Only {user} and {server} placeholders are supported.")
            DiscordClient-->>Admin: Ephemeral validation error
        else Template valid
            TemplateFormatter-->>WelcomePlugin: preview string
            WelcomePlugin->>WelcomePlugin: _update(guild.id, welcome_message=message)
            WelcomePlugin-->>DiscordClient: ctx.respond("Welcome message updated!\n**Preview:** {preview}")
            DiscordClient-->>Admin: Ephemeral success with preview
        end
    end
Loading

Sequence diagram for member greeting DM with robust error handling

sequenceDiagram
    participant DiscordGateway
    participant ModerationPlugin
    participant Member
    participant DiscordAPI
    participant Logger

    DiscordGateway->>ModerationPlugin: greet_member(member)
    ModerationPlugin->>ModerationPlugin: content = _welcome_message(member)
    ModerationPlugin->>DiscordAPI: member.send(content)
    alt DM succeeds
        DiscordAPI-->>ModerationPlugin: Message sent
        ModerationPlugin-->>DiscordGateway: return
    else discord.Forbidden
        DiscordAPI-->>ModerationPlugin: raise Forbidden
        ModerationPlugin-->>DiscordGateway: return (silently ignore)
    else discord.HTTPException
        DiscordAPI-->>ModerationPlugin: raise HTTPException
        ModerationPlugin->>Logger: logger.warning("Failed sending welcome DM", member, exc_info=True)
        Logger-->>ModerationPlugin: Logged
        ModerationPlugin-->>DiscordGateway: return
    else Unexpected Exception
        DiscordAPI-->>ModerationPlugin: raise Exception
        ModerationPlugin->>Logger: logger.exception("Failed sending welcome DM", member, exc)
        Logger-->>ModerationPlugin: Logged with traceback
        ModerationPlugin-->>DiscordGateway: Propagate exception
    end
Loading

Updated class diagram for shared helpers and LevelsPlugin config methods

classDiagram
    class SharedPluginHelpers {
        +require_guild(ctx: object) discord.Guild | None
        +read_json_file(path: Path) dict
        +write_json_file(path: Path, data: dict) None
    }

    class LevelsPlugin {
        +_build_ranks_embed(ctx, config: dict) discord.Embed
        +_remove_config_value(guild_id: int, section: str, key: int) Any | None
    }

    class ConfigStore {
        +update_config(guild_id: int, updater: function) Any | None
    }

    LevelsPlugin --> ConfigStore : uses
Loading

File-Level Changes

Change Details Files
Forward additional discord.py app command metadata through bot decorators and cover it with tests.
  • Extend slash command test to verify allowed_contexts and allowed_installs are propagated to the underlying command object.
  • Extend user command test to verify allowed_contexts and allowed_installs are propagated to the underlying command object.
  • Remove unused nsfw/allowed_* parameters from the internal _build_slash_callback helper signature to match actual usages.
tests/test_bot.py
easycord/_bot_commands.py
Prefer bulk plugin registration paths and document their usage.
  • Add a test ensuring load_default_plugins uses add_plugins instead of add_plugin when both are available.
  • Adjust server_commands package docstring to export load_default_plugins alongside the default plugins.
  • Update docs to demonstrate bulk add_plugins usage instead of load_default_plugins in the forking example, and to describe Bot.add_group(s) in API and composer tables.
tests/test_server_commands.py
server_commands/__init__.py
docs/fork-and-expand.md
docs/api.md
docs/getting-started.md
docs/release-notes.md
Harden plugin helpers and bundled plugins against malformed data and excessive exception swallowing.
  • Make require_guild explicitly typed and return a discord.Guild or None.
  • Make read_json_file resilient to missing files, I/O errors, invalid JSON, and non-dict payloads by returning an empty mapping in those cases.
  • Validate level/rank config keys as integers before building leaderboard/ranks embeds, skipping malformed keys.
  • Ensure _remove_config_value and its updater return type hints are explicit and consistent.
  • Validate and preview welcome/goodbye message templates before writing them, surfacing a friendly error on unsupported placeholders.
  • Replace broad exception swallowing in the moderation greet_member handler with targeted handling of discord.Forbidden/HTTPException and structured logging for unexpected exceptions.
easycord/plugins/_shared.py
easycord/plugins/levels.py
easycord/plugins/welcome.py
server_commands/moderation.py
Fix example runtime integration and improve group/composer usage tests.
  • Adjust basic and plugin examples to import run_bot from examples._runtime instead of a local _runtime module.
  • Fix group_bot example to call run_bot(build_bot()) with the correct single-argument runtime helper signature and drop read_token usage.
  • Add an init to the examples package so examples._runtime can be imported reliably.
  • Update the composer group test to assert internal _groups state before build() and then verify the resulting bot plugins list.
examples/basic_bot.py
examples/plugin_bot.py
examples/group_bot.py
examples/__init__.py
tests/test_composer.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!

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

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bc0ad8a2-70ff-40e9-a785-3be19caee1d0

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3729a and cc528aa.

📒 Files selected for processing (17)
  • docs/api.md
  • docs/fork-and-expand.md
  • docs/getting-started.md
  • docs/release-notes.md
  • easycord/_bot_commands.py
  • easycord/plugins/_shared.py
  • easycord/plugins/levels.py
  • easycord/plugins/welcome.py
  • examples/__init__.py
  • examples/basic_bot.py
  • examples/group_bot.py
  • examples/plugin_bot.py
  • server_commands/__init__.py
  • server_commands/moderation.py
  • tests/test_bot.py
  • tests/test_composer.py
  • tests/test_server_commands.py

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added add_group() and add_groups() methods to register SlashGroup namespaces on Bot and Composer.
  • Bug Fixes

    • Welcome/goodbye messages now validate templates before saving; invalid placeholders return clear error messages.
    • Levels leaderboard no longer crashes when rank keys contain non-numeric values.
    • Improved error handling for file I/O and JSON parsing failures.
    • Enhanced greeting message error handling to distinguish permission and transient API failures.
  • Documentation

    • Updated guides on plugin loading and startup configuration.
    • Added Windows-specific pytest troubleshooting note.

Walkthrough

Documentation updated for SlashGroup registration methods on Bot and Composer. Plugin error handling enhanced with validation and exception specificity. Example bot imports refactored. Plugin loading guidance revised. New tests verify group registration and default plugin loading.

Changes

Cohort / File(s) Summary
Documentation
docs/api.md, docs/fork-and-expand.md, docs/getting-started.md, docs/release-notes.md, server_commands/__init__.py
API docs expanded to document add_group()/add_groups() methods for Bot and Composer. Fork example updated to show direct plugin registration via bot.add_plugins(). Getting-started example includes import os. Release notes updated with date and Windows pytest note. Module docstring example clarified.
Plugin Improvements
easycord/plugins/levels.py, easycord/plugins/welcome.py, easycord/plugins/_shared.py
Levels ranks embed filters non-numeric keys before sorting. Welcome/goodbye message commands validate templates before persisting, returning errors for unsupported placeholders. Shared utilities add type annotations and graceful error handling for file I/O and JSON parsing.
Core Bot Changes
easycord/_bot_commands.py
Internal _build_slash_callback helper removes nsfw, allowed_contexts, and allowed_installs parameters from signature.
Exception Handling
server_commands/moderation.py
Announce command removes redundant per-call discord import. Greet member welcome-DM distinguishes Discord-specific failures (Forbidden, HTTPException) with targeted logging instead of swallowing all exceptions.
Example Bots
examples/__init__.py, examples/basic_bot.py, examples/group_bot.py, examples/plugin_bot.py
Module docstring added to examples. Import paths updated to source run_bot from examples._runtime. Group bot removes read_token() call, delegating to run_bot.
Tests
tests/test_bot.py, tests/test_composer.py, tests/test_server_commands.py
Command metadata tests add allowed_contexts and allowed_installs parameters with assertions. Composer test verifies _groups is populated before build(). New test validates load_default_plugins calls add_plugins once with correct plugin order.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Groups now register with grace,
Templates validated in their place,
Error handling refined with care,
Examples hop with imports fair,
Plugin loading lights the way! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex-easycord-refactor-features

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Ruff (0.15.10)
tests/test_server_commands.py

Unexpected Ruff issue shape at index 0


Comment @coderabbitai help to get the list of available commands and usage tips.

@rolling-codes rolling-codes merged commit 96c7094 into main Apr 22, 2026
2 of 5 checks passed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In easycord/plugins/_shared.read_json_file, swallowing all OSError/ValueError and returning {} may make filesystem or JSON issues hard to debug; consider logging or otherwise surfacing the error context before falling back to an empty mapping.
  • Changing the example scripts to from examples._runtime import run_bot means they now assume the project root is on sys.path and are less friendly to running as python examples/basic_bot.py; consider using a relative import or adding a small compatibility shim so they work both when run as a module and as a script.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `easycord/plugins/_shared.read_json_file`, swallowing all `OSError`/`ValueError` and returning `{}` may make filesystem or JSON issues hard to debug; consider logging or otherwise surfacing the error context before falling back to an empty mapping.
- Changing the example scripts to `from examples._runtime import run_bot` means they now assume the project root is on `sys.path` and are less friendly to running as `python examples/basic_bot.py`; consider using a relative import or adding a small compatibility shim so they work both when run as a module and as a script.

## Individual Comments

### Comment 1
<location path="easycord/plugins/_shared.py" line_range="25-30" />
<code_context>
         return {}
-    with path.open(encoding="utf-8") as handle:
-        return json.load(handle)
+    try:
+        with path.open(encoding="utf-8") as handle:
+            data = json.load(handle)
+    except (OSError, ValueError):
+        return {}
+    return data if isinstance(data, dict) else {}


</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing JSON/IO errors and returning an empty dict may hide configuration corruption.

This change treats any `OSError`/`ValueError` during read, or any non-dict JSON, as “no config”. That simplifies callers but can silently mask corrupted or partially written config files, making issues hard to diagnose. Consider at least logging the error, distinguishing “file missing” from “file corrupted”, or providing a mode where exceptions are surfaced to the caller.

Suggested implementation:

```python
    """Read a JSON file if it exists, otherwise return an empty mapping."""
    if not path.exists():
        # Explicit "no config" case: file is not present on disk.
        return {}

```

```python
    if not path.exists():
        # Explicit "no config" case: file is not present on disk.
        return {}

    try:
        with path.open(encoding="utf-8") as handle:
            data = json.load(handle)
    except FileNotFoundError:
        # File was deleted between the exists() check and open(); treat as "no config".
        return {}
    except (OSError, ValueError) as exc:
        # Distinguish corrupted/unreadable config from "no config" by logging the failure.
        logger = logging.getLogger(__name__)
        logger.warning("Failed to load JSON config from %s: %s", path, exc, exc_info=True)
        return {}

    if not isinstance(data, dict):
        # Non-dict JSON is unexpected for a mapping-based config; log and treat as missing.
        logger = logging.getLogger(__name__)
        logger.warning(
            "Expected JSON object when loading config from %s, got %r",
            path,
            type(data).__name__,
        )
        return {}

    return data

```

1. Ensure `import logging` is present near the top of `easycord/plugins/_shared.py`. If it is not, add:
   `import logging`
2. If this helper is used for non-config JSON as well, consider adjusting the log messages to reflect the broader usage (e.g. "JSON file" instead of "config").
3. If you later decide to expose a "strict" mode (surfacing exceptions to callers), extend the function signature with a keyword like `strict: bool = False` and, inside the `except (OSError, ValueError)` and non-dict branches, `raise` when `strict` is `True` instead of returning `{}`.
</issue_to_address>

### Comment 2
<location path="tests/test_composer.py" line_range="128-131" />
<code_context>

     a, b = A(), B()
-    bot = Composer().add_groups(a, b).build()
+    composer = Composer().add_groups(a, b)
+    assert composer._groups == [a, b]
+
+    bot = composer.build()
     assert bot._plugins == [a, b]

</code_context>
<issue_to_address>
**suggestion (testing):** Expand Composer group tests to cover edge cases like repeated or empty add_groups calls.

The existing assertions are a good sanity check for order preservation through `build()`. To further harden the tests, add cases that:

- Call `add_groups()` multiple times and verify groups are accumulated in the correct order.
- Call `add_groups()` with no arguments and with duplicate group instances, and assert the expected behavior (no-op, error, or allowing duplicates).

This will make `Composer` behavior clearer and future refactors safer.
</issue_to_address>

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.

Comment on lines +25 to +30
try:
with path.open(encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, ValueError):
return {}
return data if isinstance(data, dict) else {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Swallowing JSON/IO errors and returning an empty dict may hide configuration corruption.

This change treats any OSError/ValueError during read, or any non-dict JSON, as “no config”. That simplifies callers but can silently mask corrupted or partially written config files, making issues hard to diagnose. Consider at least logging the error, distinguishing “file missing” from “file corrupted”, or providing a mode where exceptions are surfaced to the caller.

Suggested implementation:

    """Read a JSON file if it exists, otherwise return an empty mapping."""
    if not path.exists():
        # Explicit "no config" case: file is not present on disk.
        return {}
    if not path.exists():
        # Explicit "no config" case: file is not present on disk.
        return {}

    try:
        with path.open(encoding="utf-8") as handle:
            data = json.load(handle)
    except FileNotFoundError:
        # File was deleted between the exists() check and open(); treat as "no config".
        return {}
    except (OSError, ValueError) as exc:
        # Distinguish corrupted/unreadable config from "no config" by logging the failure.
        logger = logging.getLogger(__name__)
        logger.warning("Failed to load JSON config from %s: %s", path, exc, exc_info=True)
        return {}

    if not isinstance(data, dict):
        # Non-dict JSON is unexpected for a mapping-based config; log and treat as missing.
        logger = logging.getLogger(__name__)
        logger.warning(
            "Expected JSON object when loading config from %s, got %r",
            path,
            type(data).__name__,
        )
        return {}

    return data
  1. Ensure import logging is present near the top of easycord/plugins/_shared.py. If it is not, add:
    import logging
  2. If this helper is used for non-config JSON as well, consider adjusting the log messages to reflect the broader usage (e.g. "JSON file" instead of "config").
  3. If you later decide to expose a "strict" mode (surfacing exceptions to callers), extend the function signature with a keyword like strict: bool = False and, inside the except (OSError, ValueError) and non-dict branches, raise when strict is True instead of returning {}.

Comment thread tests/test_composer.py
Comment on lines +128 to +131
composer = Composer().add_groups(a, b)
assert composer._groups == [a, b]

bot = composer.build()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Expand Composer group tests to cover edge cases like repeated or empty add_groups calls.

The existing assertions are a good sanity check for order preservation through build(). To further harden the tests, add cases that:

  • Call add_groups() multiple times and verify groups are accumulated in the correct order.
  • Call add_groups() with no arguments and with duplicate group instances, and assert the expected behavior (no-op, error, or allowing duplicates).

This will make Composer behavior clearer and future refactors safer.

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.

2 participants