Skip to content

feat: Plugin Power Pack — dependency declarations, analytics middleware, per-guild flags (v5.52.0)#73

Merged
rolling-codes merged 1 commit into
mainfrom
feature/plugin-power-pack
Jul 3, 2026
Merged

feat: Plugin Power Pack — dependency declarations, analytics middleware, per-guild flags (v5.52.0)#73
rolling-codes merged 1 commit into
mainfrom
feature/plugin-power-pack

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Three structural additions that close framework gaps vs hikari/lightbulb and production-grade Discord bots:

  • Plugin dependency declarationsPlugin.requires = ("economy",) on a subclass; bot.add_plugin() raises PluginDependencyError (with .missing and .plugin_class) if any declared dep isn't loaded yet
  • Analytics middlewarebot.use(analytics_middleware()) auto-wires an AnalyticsStore; bot.command_stats(guild_id=None) returns aggregate or per-guild invocation counts with zero additional code
  • Per-guild plugin feature flagsbot.disable_plugin(name, guild_id) / enable_plugin / is_plugin_enabled; disabled commands return an ephemeral block without executing the handler; DM invocations are always unaffected

New public exports from easycord: PluginDependencyError, AnalyticsStore, analytics_middleware.

Files changed

File Change
easycord/plugin.py Add requires: tuple[str, ...] class attribute
easycord/_bot_plugins.py Add PluginDependencyError, dependency check in add_plugin(), three feature-flag methods
easycord/middleware.py Add AnalyticsStore dataclass + analytics_middleware() factory
easycord/_bot_events.py Auto-wire analytics store in use()
easycord/bot.py Add _analytics_store, _guild_disabled_plugins init vars + command_stats() method
easycord/_command_callbacks.py Per-guild flag guard before command dispatch
easycord/__init__.py Export PluginDependencyError, AnalyticsStore, analytics_middleware
tests/test_plugin_power_pack.py 24 new tests (unit + 4 end-to-end dispatch guard integration tests)
tests/test_middleware.py 11 new tests for AnalyticsStore and analytics_middleware
CHANGELOG.md, pyproject.toml, easycord/__init__.py, README.md, docs/getting-started.md Bump to v5.52.0
release_v5.52.0/notes.md Release notes with working code snippets

Test plan

  • ruff check --select E9,F63,F7,F82 easycord tests — passes (blocking lint gate)
  • pytest tests/ -q — 1438 passed (up from 1335; 35 new tests)
  • python scripts/check_release_metadata.py — passes
  • python scripts/verify_plugin_tests.py — passes
  • tests/test_plugin_power_pack.py::TestPerGuildFlagDispatchGuard — 4 integration tests exercise the full dispatch path with real Bot + invoke():
    • disabled plugin → ephemeral block
    • disabled in guild A → unaffected in guild B
    • enable restores dispatch
    • DM invocations never blocked
  • No breaking changes — all existing tests pass unchanged

Summary by Sourcery

Add plugin dependency declarations, analytics middleware with a shared analytics store, and per-guild plugin enable/disable flags, and release them as EasyCord v5.52.0.

New Features:

  • Allow plugins to declare required plugin names via a class-level requires attribute and enforce load order when adding plugins to the bot.
  • Introduce an in-memory AnalyticsStore and analytics_middleware to automatically track and expose per-command invocation counts via Bot.command_stats.
  • Add per-guild plugin feature flags on the bot to disable or enable all commands from a given plugin on a specific server while leaving DMs unaffected.

Enhancements:

  • Wire analytics middleware into the bot so that registering it automatically connects an internal analytics store for later stats queries.
  • Guard command dispatch with a per-guild plugin enabled check, returning a localized ephemeral message when a plugin is disabled for that guild.

Build:

  • Bump project metadata and versioning to v5.52.0 in pyproject and package exports.

Documentation:

  • Update changelog, README, getting-started guide, and new release notes to document the Plugin Power Pack features and bump the version to v5.52.0.

Tests:

  • Add comprehensive unit and integration tests for plugin dependency enforcement, per-guild plugin flags, and analytics middleware behavior, increasing total test count.

…re, per-guild flags

Three structural additions that close framework gaps vs hikari/lightbulb:

1. Plugin.requires + PluginDependencyError — declare load-order requirements on
   Plugin subclasses; add_plugin() raises PluginDependencyError (with .missing
   and .plugin_class attrs) if any declared dep is not already loaded.

2. AnalyticsStore + analytics_middleware() — zero-config invocation counting;
   bot.use(analytics_middleware()) auto-wires the store; bot.command_stats()
   queries aggregate or per-guild counts.

3. Per-guild plugin feature flags — bot.disable_plugin/enable_plugin/is_plugin_enabled
   let server admins selectively block plugin commands; disabled commands return
   an ephemeral response without executing the handler; DM invocations unaffected.

Exports added to easycord: PluginDependencyError, AnalyticsStore, analytics_middleware.
Bump to v5.52.0. 1438 tests (35 new, including 4 end-to-end dispatch guard tests).
@sourcery-ai

sourcery-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the Plugin Power Pack: plugin dependency declarations enforced at add-time, analytics middleware with an attached AnalyticsStore and Bot.command_stats helper, and per-guild plugin feature flags that gate command dispatch, plus exports/docs/versioning and comprehensive tests.

Sequence diagram for analytics middleware wiring and stats

sequenceDiagram
    actor Dev
    participant Bot
    participant EventsMixin as _EventsMixin
    participant Middleware as analytics_middleware
    participant Store as AnalyticsStore

    Dev->>Bot: use(analytics_middleware())
    Bot->>Middleware: analytics_middleware()
    Middleware-->>Bot: handler (MiddlewareFn)
    Note over Middleware,Store: handler._analytics_store = AnalyticsStore
    Bot->>EventsMixin: use(handler)
    EventsMixin->>EventsMixin: append middleware
    EventsMixin->>Bot: set _analytics_store

    Dev->>Bot: command_stats(guild_id)
    Bot->>Store: query(guild_id)
    Store-->>Bot: dict[str, int]
    Bot-->>Dev: stats
Loading

Sequence diagram for per-guild plugin disable guard

sequenceDiagram
    participant Discord as discord.Interaction
    participant Callback as callback
    participant Bot
    participant Registry as registry
    participant Plugin as plugin

    Discord->>Callback: invoke callback(...)
    Callback->>Callback: build Context ctx
    Callback->>Bot: is_plugin_enabled? via registry lookup
    Callback->>Registry: slash_commands.get(...)
    Registry-->>Callback: entry (with source)
    Callback->>Bot: is_plugin_enabled(plugin_name, ctx.guild.id)
    alt plugin disabled
        Callback->>Callback: ctx.respond("errors.plugin_disabled", ephemeral=True)
        Callback-->>Discord: end
    else plugin enabled or DM
        Callback->>Callback: invoke()
        Callback-->>Discord: handler result
    end
Loading

File-Level Changes

Change Details Files
Enforce declarative plugin dependencies during plugin registration.
  • Introduce PluginDependencyError(RuntimeError) carrying plugin_class and missing dependency names.
  • Add Plugin.requires class attribute on Plugin for declaring load-order requirements.
  • Update PluginsMixin.add_plugin to compute loaded plugin names, detect missing requirements, and raise PluginDependencyError before scanning methods or registering the plugin.
easycord/_bot_plugins.py
easycord/plugin.py
Add in-memory analytics store and middleware, and expose bot-level command statistics.
  • Add AnalyticsStore dataclass that records per-(command_name, guild_id
None) counters and supports global or per-guild queries.
  • Implement analytics_middleware factory that records on each invocation, auto-creates or accepts a store, and exposes it via a _analytics_store attribute on the middleware function.
  • Wire bot.use() to detect middleware._analytics_store and assign it to bot._analytics_store, and add Bot._analytics_store field plus command_stats(guild_id=None) wrapper for querying analytics.
  • Export AnalyticsStore and analytics_middleware from the easycord package.
  • Add unit and integration tests for AnalyticsStore and analytics_middleware behavior.
  • Introduce per-guild plugin feature flags and enforce them in command dispatch.
    • Add _guild_disabled_plugins state to Bot and implement disable_plugin, enable_plugin, and is_plugin_enabled methods on the plugins mixin using a dict[guild_id, set[plugin_name]].
    • Modify command callback dispatch to resolve the source plugin for a slash command via the registry, check is_plugin_enabled for the current guild, and short-circuit with an ephemeral "feature disabled" response when the plugin is disabled, while leaving DMs unaffected.
    • Add unit tests for per-guild enable/disable behavior on a stub bot and end-to-end integration tests using a real Bot and invoke() to validate blocking, isolation between guilds, re-enabling, and DM behavior.
    easycord/_bot_plugins.py
    easycord/bot.py
    easycord/_command_callbacks.py
    tests/test_plugin_power_pack.py
    Update public API surface, release metadata, and documentation for v5.52.0.
    • Export PluginDependencyError from easycord.init and bump version to 5.52.0.
    • Update README, docs/getting-started, CHANGELOG, pyproject metadata, and new release notes to reference v5.52.0, new download URLs, and the Plugin Power Pack features.
    easycord/__init__.py
    CHANGELOG.md
    pyproject.toml
    README.md
    docs/getting-started.md
    release_v5.52.0/notes.md

    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

    @github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies tests enhancement New feature or request labels Jul 3, 2026
    @coderabbitai

    coderabbitai Bot commented Jul 3, 2026

    Copy link
    Copy Markdown

    Warning

    Review limit reached

    @rolling-codes, you've reached your PR review limit, so we couldn't start this review.

    Next review available in: 8 minutes

    Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
    You're only billed for reviews past your plan's rate limits ($0.25/file).

    How can I continue?

    After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

    To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

    How do review limits work?

    CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

    For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

    Please refer docs for additional details.

    Review details
    ⚙️ Run configuration

    Configuration used: Organization UI

    Review profile: ASSERTIVE

    Plan: Pro

    Run ID: 6412ac14-d012-4d65-b0be-a99072f9ff13

    📥 Commits

    Reviewing files that changed from the base of the PR and between a21d904 and 95daab4.

    📒 Files selected for processing (14)
    • CHANGELOG.md
    • README.md
    • docs/getting-started.md
    • easycord/__init__.py
    • easycord/_bot_events.py
    • easycord/_bot_plugins.py
    • easycord/_command_callbacks.py
    • easycord/bot.py
    • easycord/middleware.py
    • easycord/plugin.py
    • pyproject.toml
    • release_v5.52.0/notes.md
    • tests/test_middleware.py
    • tests/test_plugin_power_pack.py
    ✨ Finishing Touches
    🧪 Generate unit tests (beta)
    • Create PR with unit tests
    • Commit unit tests in branch feature/plugin-power-pack

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    ❤️ Share

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

    @codecov

    codecov Bot commented Jul 3, 2026

    Copy link
    Copy Markdown

    Codecov Report

    ❌ Patch coverage is 92.10526% with 6 lines in your changes missing coverage. Please review.

    Files with missing lines Patch % Lines
    easycord/_bot_events.py 25.00% 3 Missing ⚠️
    easycord/bot.py 57.14% 3 Missing ⚠️

    📢 Thoughts on this report? Let us know!

    @qodo-code-review

    Copy link
    Copy Markdown

    PR Summary by Qodo

    Plugin Power Pack: plugin deps, analytics middleware, per-guild plugin flags

    ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

    Grey Divider

    AI Description

    • Enforce plugin load order via Plugin.requires and PluginDependencyError in add_plugin().
    • Add zero-config analytics middleware and bot.command_stats() for per-guild/global counts.
    • Add per-guild plugin enable/disable with a dispatch-time guard; bump release to v5.52.0.
    
    Diagram

    graph TD
      P["Plugin subclass"] --> PM["Plugin manager"] --> B(["Bot"])
      PM --> DE{{"PluginDependencyError"}}
      B --> AMW["analytics_middleware()"] --> AS[("AnalyticsStore")]
      B --> D["Command dispatch"] --> F["Per-guild flags"]
      B --> AS
    
      subgraph Legend
        direction LR
        _svc(["Service/Component"]) ~~~ _dec{{"Decision/Error"}} ~~~ _db[("Store")]
      end
    
    Loading
    High-Level Assessment

    The following are alternative approaches to this PR:

    1. Auto-load dependencies (topological plugin loader)
    • ➕ Developer ergonomics: add_plugin() can load dependencies automatically
    • ➕ Can detect cycles and give a full dependency graph error
    • ➖ Requires a registry/factory for plugin instances, not just instances
    • ➖ More complex behavior and potentially surprising side effects
    2. Explicit analytics registration API (no attribute introspection)
    • ➕ Clearer contract than setting _analytics_store on a function
    • ➕ Avoids coupling use() behavior to middleware implementation details
    • ➖ More boilerplate for users (extra call/property assignment)
    • ➖ Harder to keep 'zero-config' promise
    3. Persist per-guild flags via ServerConfigStore integration
    • ➕ Survives restarts and supports multi-process deployments
    • ➕ Provides a natural admin UX and auditing path
    • ➖ Introduces persistence semantics and migration concerns
    • ➖ More invasive changes across config/database layers

    Recommendation: Current approach is a good v1: it’s backward-compatible, keeps user-facing APIs minimal, and is well-covered by unit + dispatch-path integration tests. The two 'magic' pieces (middleware attribute introspection and in-memory per-guild flags) are acceptable for a feature release; consider a follow-up to optionally persist flags and/or formalize analytics registration if more middleware-to-bot wiring is added.

    Files changed (14) +731 / -10

    Enhancement (7) +192 / -3
    __init__.pyExport new public APIs and bump package version +3/-1

    Export new public APIs and bump package version

    • Updates '__version__' to 5.52.0. Exposes 'PluginDependencyError', 'AnalyticsStore', and 'analytics_middleware' from the top-level 'easycord' package.
    

    easycord/init.py

    _bot_events.pyAuto-wire analytics store when middleware is registered +4/-1

    Auto-wire analytics store when middleware is registered

    • Extends 'use()' to detect an 'AnalyticsStore' attached to middleware and store it on the bot. Enables 'bot.command_stats()' without additional user wiring.
    

    easycord/_bot_events.py

    _bot_plugins.pyAdd plugin dependency enforcement and per-guild enable/disable APIs +62/-0

    Add plugin dependency enforcement and per-guild enable/disable APIs

    • Introduces 'PluginDependencyError' and checks 'Plugin.requires' during 'add_plugin()' to enforce load ordering. Adds in-memory per-guild plugin feature flag methods: 'disable_plugin', 'enable_plugin', and 'is_plugin_enabled'.
    

    easycord/_bot_plugins.py

    _command_callbacks.pyGuard command dispatch when a plugin is disabled in a guild +28/-0

    Guard command dispatch when a plugin is disabled in a guild

    • Adds a pre-invocation check that resolves a command’s source plugin and blocks execution when disabled for the invoking guild. Returns an ephemeral localized 'feature disabled' response; DMs bypass the guard.
    

    easycord/_command_callbacks.py

    bot.pyTrack analytics store and expose 'command_stats()' +21/-1

    Track analytics store and expose 'command_stats()'

    • Initializes '_analytics_store' and '_guild_disabled_plugins' on bot construction. Adds 'command_stats(guild_id=None)' to query aggregate or per-guild command invocation counts when analytics middleware is enabled.
    

    easycord/bot.py

    middleware.pyAdd 'AnalyticsStore' and 'analytics_middleware()' +62/-0

    Add 'AnalyticsStore' and 'analytics_middleware()'

    • Adds an in-memory 'AnalyticsStore' dataclass plus a middleware factory that records command invocations by guild. The middleware attaches the store for bot auto-wiring via 'use()'.
    

    easycord/middleware.py

    plugin.pyIntroduce 'Plugin.requires' dependency declaration +12/-0

    Introduce 'Plugin.requires' dependency declaration

    • Adds a 'requires: tuple[str, ...]' class attribute and documentation on declaring plugin dependencies. Establishes the contract that missing deps raise 'PluginDependencyError' during registration.
    

    easycord/plugin.py

    Tests (2) +425 / -0
    test_middleware.pyAdd unit + integration tests for analytics store and middleware +118/-0

    Add unit + integration tests for analytics store and middleware

    • Adds tests for 'AnalyticsStore.record/query' behavior across guild/DM contexts and aggregation rules. Includes async middleware tests verifying recording behavior and store auto-creation/attachment.
    

    tests/test_middleware.py

    test_plugin_power_pack.pyAdd tests for plugin deps, flags, and dispatch guard integration +307/-0

    Add tests for plugin deps, flags, and dispatch guard integration

    • Adds unit tests for 'Plugin.requires' enforcement and 'PluginDependencyError' attributes. Adds per-guild flag unit tests plus end-to-end dispatch tests using a real 'Bot' + 'invoke()' to verify guild scoping and DM bypass.
    

    tests/test_plugin_power_pack.py

    Documentation (4) +111 / -4
    CHANGELOG.mdDocument v5.52.0 Plugin Power Pack features and tests +28/-0

    Document v5.52.0 Plugin Power Pack features and tests

    • Adds a v5.52.0 entry describing plugin dependency declarations, analytics middleware/store, and per-guild feature flags. Notes new exports and expanded test coverage totals.
    

    CHANGELOG.md

    README.mdBump README version badge and install links to v5.52.0 +3/-3

    Bump README version badge and install links to v5.52.0

    • Updates the displayed version badge, release link, and wheel download URL to v5.52.0.
    

    README.md

    getting-started.mdUpdate getting-started install command to v5.52.0 +1/-1

    Update getting-started install command to v5.52.0

    • Bumps the GitHub release wheel URL from v5.51.0 to v5.52.0.
    

    docs/getting-started.md

    notes.mdAdd v5.52.0 release notes with usage examples +79/-0

    Add v5.52.0 release notes with usage examples

    • Introduces release notes describing the three new framework capabilities with code snippets. Includes installation and upgrade notes emphasizing no breaking changes.
    

    release_v5.52.0/notes.md

    Other (1) +3 / -3
    pyproject.tomlBump package version and project URLs to v5.52.0 +3/-3

    Bump package version and project URLs to v5.52.0

    • Updates project version to 5.52.0 and refreshes release/download URLs accordingly.
    

    pyproject.toml

    ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

    """Tests for Plugin Power Pack: dependency declarations and per-guild feature flags."""
    from __future__ import annotations

    from unittest.mock import AsyncMock, MagicMock
    class DepA(Plugin):
    pass

    class DepB(Plugin):

    @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 1 issue

    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Individual Comments
    
    ### Comment 1
    <location path="release_v5.52.0/notes.md" line_range="7" />
    <code_context>
    +
    +## Overview
    +
    +Feature release adding the Plugin Power Pack: three structural additions that close gaps compared to hikari/lightbulb and production-grade Discord bots — safe plugin load ordering, zero-config command analytics, and per-guild plugin toggling.
    +
    +## New Features
    </code_context>
    <issue_to_address>
    **nitpick (typo):** Capitalize proper names "hikari" and "lightbulb" for consistency.
    
    Use the capitalized forms "Hikari" and "Lightbulb" (e.g., "Hikari/Lightbulb") to match the official project names and stay consistent with the rest of the documentation.
    
    ```suggestion
    Feature release adding the Plugin Power Pack: three structural additions that close gaps compared to Hikari/Lightbulb and production-grade Discord bots — safe plugin load ordering, zero-config command analytics, and per-guild plugin toggling.
    ```
    </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 thread release_v5.52.0/notes.md

    ## Overview

    Feature release adding the Plugin Power Pack: three structural additions that close gaps compared to hikari/lightbulb and production-grade Discord bots — safe plugin load ordering, zero-config command analytics, and per-guild plugin toggling.

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    nitpick (typo): Capitalize proper names "hikari" and "lightbulb" for consistency.

    Use the capitalized forms "Hikari" and "Lightbulb" (e.g., "Hikari/Lightbulb") to match the official project names and stay consistent with the rest of the documentation.

    Suggested change
    Feature release adding the Plugin Power Pack: three structural additions that close gaps compared to hikari/lightbulb and production-grade Discord bots — safe plugin load ordering, zero-config command analytics, and per-guild plugin toggling.
    Feature release adding the Plugin Power Pack: three structural additions that close gaps compared to Hikari/Lightbulb and production-grade Discord bots — safe plugin load ordering, zero-config command analytics, and per-guild plugin toggling.

    @rolling-codes rolling-codes merged commit cedf08c into main Jul 3, 2026
    16 checks passed
    @rolling-codes rolling-codes deleted the feature/plugin-power-pack branch July 3, 2026 21:48
    @qodo-code-review

    Copy link
    Copy Markdown

    Code Review by Qodo

    🐞 Bugs (2) 📘 Rule violations (4) 📜 Skill insights (0)

    Context used
    ✅ Compliance rules (platform): 36 rules

    Grey Divider


    Action required

    1. Per-guild state stored in-memory 📘 Rule violation ⌂ Architecture
    Description
    The PR introduces per-guild feature-flag state and per-guild analytics counters stored on the Bot
    instance (_guild_disabled_plugins, _analytics_store) without any database persistence. This
    violates the requirement that long-lived per-guild state must be persisted in the database rather
    than held in-process on the bot.
    
    Code

    easycord/bot.py[R118-119]

    +        self._analytics_store: AnalyticsStore | None = None
    +        self._guild_disabled_plugins: dict[int, set[str]] = {}
    Relevance

    ⭐⭐ Medium

    No clear prior review history on persisting per-guild state vs in-memory bot dicts in this repo.
    

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    Rule 526285 requires per-guild state not be stored as bot instance dictionaries keyed by guild_id.
    The PR adds _guild_disabled_plugins: dict[int, set[str]] and _analytics_store on Bot, and
    implements per-guild feature flags and per-guild command counters purely in memory (no DB
    reads/writes).
    

    Rule 526285: Per-guild state must be persisted in the database, not on the Bot instance
    easycord/bot.py[118-119]
    easycord/_bot_plugins.py[378-408]
    easycord/middleware.py[330-365]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    Per-guild feature flags (`_guild_disabled_plugins`) and per-guild analytics counters (`_analytics_store` / `AnalyticsStore.counts`) are stored only in memory on the `Bot` instance, so they are lost on restart and violate the compliance rule requiring DB-backed per-guild state.
    
    ## Issue Context
    The PR explicitly implements per-guild plugin enable/disable and per-guild command stats. These are durable, guild-scoped data structures keyed by `guild_id`, and therefore must be persisted via the project’s database layer (repository/DAO or GuildRecord-style row), not as a Python dict on the bot.
    
    ## Fix Focus Areas
    - easycord/bot.py[118-119]
    - easycord/_bot_plugins.py[378-408]
    - easycord/_bot_events.py[74-93]
    - easycord/middleware.py[330-388]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



    Informational

    2. Bot.command_stats() added in bot.py 📘 Rule violation ⌂ Architecture
    Description
    The PR adds new bot-level analytics behavior directly in easycord/bot.py via
    Bot.command_stats(), rather than implementing it in an appropriate _bot_*.py mixin. This
    violates the rule requiring new bot behavior to live in mixin modules, with bot.py limited to
    composition/wiring.
    
    Code

    easycord/bot.py[R142-159]

    +    # ── Analytics ────────────────────────────────────────────
    +
    +    def command_stats(self, guild_id: int | None = None) -> dict[str, int]:
    +        """Return slash command invocation counts recorded by :func:`analytics_middleware`.
    +
    +        *guild_id* filters to one server; omit it to sum across all guilds and DMs.
    +        Returns an empty dict if :func:`analytics_middleware` has not been registered.
    +
    +        Example::
    +
    +            bot.use(analytics_middleware())
    +            stats = bot.command_stats(guild_id=123)  # {"ping": 5}
    +            total = bot.command_stats()              # sum across all guilds
    +        """
    +        if self._analytics_store is None:
    +            return {}
    +        return self._analytics_store.query(guild_id)
    +
    Relevance

    ⭐ Low

    Similar request to move new Bot methods out of bot.py into mixins was rejected historically.
    

    PR-#39

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    Rule 526288 requires new bot-level behavior to be implemented in mixin files (e.g.,
    _bot_commands.py, _bot_events.py) rather than directly in bot.py. The PR introduces the new
    command_stats() method (analytics behavior) directly on Bot in easycord/bot.py.
    

    Rule 526288: Add new bot-level behavior via mixin files, not directly in bot.py
    easycord/bot.py[142-159]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    `Bot.command_stats()` is new bot behavior implemented directly in `easycord/bot.py`, contrary to the mixin-only bot-behavior rule.
    
    ## Issue Context
    The project splits bot behavior across mixins (e.g., `_bot_events.py`, `_bot_plugins.py`, `_bot_commands.py`). `bot.py` should remain thin composition and wiring.
    
    ## Fix Focus Areas
    - easycord/bot.py[142-159]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


    3. Tests import easycord._bot_plugins 📘 Rule violation ⌂ Architecture
    Description
    The new test file imports an underscore-prefixed internal module (easycord._bot_plugins) from
    outside the easycord/ package. This violates the rule restricting external code (including tests)
    to the public easycord API surface.
    
    Code

    tests/test_plugin_power_pack.py[R8-10]

    +from easycord import Bot, Plugin, PluginDependencyError, slash
    +from easycord._bot_plugins import _PluginsMixin
    +from easycord.testing import invoke
    Relevance

    ⭐ Low

    Historical suggestions to stop tests importing underscore-prefixed internal modules were rejected;
    pattern likely accepted here too.
    

    PR-#39
    PR-#46

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    Rules 526296 and 1570536 disallow importing underscore-prefixed easycord modules from outside the
    easycord/ package. The test file imports _PluginsMixin from easycord._bot_plugins, which is an
    internal module path.
    

    Rule 526296: Only import public easycord API from outside the package
    Rule 1570536: Import easycord symbols only from the public top-level package
    tests/test_plugin_power_pack.py[8-10]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    `tests/test_plugin_power_pack.py` imports `easycord._bot_plugins`, which is an internal underscore-prefixed module and not part of the public API.
    
    ## Issue Context
    Compliance requires external code (including tests) to import only from the public top-level `easycord` package API, not internal `_...` modules.
    
    ## Fix Focus Areas
    - tests/test_plugin_power_pack.py[8-10]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


    4. Tests call Plugin() constructors 📘 Rule violation ▣ Testability
    Description
    The new tests instantiate Plugin subclasses via normal constructors instead of using __new__ and
    setting _bot directly as required. This can inadvertently run initialization logic and violates
    the mandated plugin construction pattern for tests.
    
    Code

    tests/test_plugin_power_pack.py[R39-61]

    +class TestPluginDependencyDeclarations:
    +    def test_no_requires_loads_fine(self) -> None:
    +        class SimplePlugin(Plugin):
    +            pass
    +
    +        bot = _make_bot()
    +        p = SimplePlugin()
    +        bot.add_plugin(p)
    +        assert p in bot._plugins
    +
    +    def test_satisfied_dep_loads_fine(self) -> None:
    +        class CorePlugin(Plugin):
    +            pass
    +
    +        class ExtPlugin(Plugin):
    +            requires = ("coreplugin",)
    +
    +        bot = _make_bot()
    +        core = CorePlugin()
    +        bot.add_plugin(core)
    +        ext = ExtPlugin()
    +        bot.add_plugin(ext)
    +        assert ext in bot._plugins
    Relevance

    ⭐ Low

    Prior test-guideline suggestion to construct plugins via __new__/manual _bot wiring was rejected;
    constructors in tests likely allowed.
    

    PR-#60

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    Rule 1073283 requires plugin instances in tests to be created via __new__ and have _bot set
    directly. The new tests create plugin instances with normal constructors (e.g., `p =
    SimplePlugin(), core = CorePlugin(), ext = ExtPlugin()`) in the added test suite.
    

    Rule 1073283: Construct Plugin instances in tests via new and set _bot directly
    tests/test_plugin_power_pack.py[39-61]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    Tests should construct `Plugin` instances using `Plugin.__new__(Subclass)` and then set `plugin._bot = ...` directly, rather than calling subclass constructors.
    
    ## Issue Context
    The compliance rule requires this pattern to keep tests lightweight and avoid constructor side effects/invariants during unit tests.
    
    ## Fix Focus Areas
    - tests/test_plugin_power_pack.py[39-61]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


    View more (2)
    5. Disable guard misses groups 🐞 Bug ≡ Correctness
    Description
    The per-guild plugin-disable guard resolves the command’s plugin by looking up
    registry.slash_commands using only the leaf command name, but grouped commands are registered
    under a qualified "<group> <subcommand>" name so the lookup can fail and the disabled plugin’s
    commands still execute. This breaks the advertised behavior for SlashGroup-based plugins in guilds.
    
    Code

    easycord/_command_callbacks.py[R110-116]

    +                _cmd = command_name or func.__name__
    +                # Registry keys are scoped: "global:<name>" for global commands,
    +                # "guild:<id>:<name>" for guild-restricted ones.
    +                _entry = (
    +                    _registry.slash_commands.get(f"global:{_cmd}")
    +                    or _registry.slash_commands.get(f"guild:{ctx.guild.id}:{_cmd}")
    +                )
    Relevance

    ⭐ Low

    Team previously rejected using group-qualified registry names for SlashGroup collisions; likely
    won’t enforce qualified lookup now.
    

    PR-#39

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    The registry stores slash command entries under keys built as <scope>:<name>, and for grouped
    commands the stored name is qualified with the parent group name ("<parent> <name>"). The new
    dispatch guard builds lookup keys using only the leaf command name, so it will not find grouped
    entries and therefore will not apply the disable check.
    

    easycord/_command_callbacks.py[101-133]
    easycord/_command_registration.py[121-166]
    easycord/registry.py[206-235]
    easycord/registry.py[377-379]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    Per-guild plugin disabling can be bypassed for grouped slash commands because the guard attempts to resolve the plugin via `InteractionRegistry.slash_commands` using a key built from the *leaf* command name only.
    
    ## Issue Context
    - Registry keys are scoped (`global:` / `guild:<id>:`) and the stored *name* for grouped commands includes the parent group prefix (e.g., `"mod kick"`).
    - The guard uses `command_name` (which is passed as the leaf name) to build lookup keys like `global:kick`, which will not match the stored `global:mod kick` entry.
    
    ## Fix focus areas
    - Prefer using the already-available bound plugin instance instead of re-resolving via the registry:
     - In `build_slash_callback`, `func` is a bound method for plugin commands, so `getattr(func, "__self__", None)` gives you the plugin instance.
     - If `__self__` is a `Plugin`, use `plugin.name` directly with `bot.is_plugin_enabled(plugin.name, ctx.guild.id)`.
     - This works for top-level plugin commands, aliases, and `SlashGroup` subcommands without any registry key guessing.
    
    ### Fix Focus Areas
    - easycord/_command_callbacks.py[101-133]
    - easycord/plugin.py[10-80]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


    6. Analytics merges subcommands 🐞 Bug ≡ Correctness
    Description
    analytics_middleware records ctx.command_name, which is only the leaf command name from
    interaction.command.name, so subcommands with the same name in different SlashGroups are merged
    into a single counter. This produces incorrect bot.command_stats() results for grouped command
    trees.
    
    Code

    easycord/middleware.py[R383-385]

    +    async def handler(ctx: Context, proceed: Callable[[], Awaitable[None]]) -> None:
    +        used_store.record(ctx.command_name, ctx.guild.id if ctx.guild else None)
    +        await proceed()
    Relevance

    ⭐ Low

    Prior reviews rejected adopting group-qualified command identifiers, implying leaf-name analytics
    collisions are tolerated.
    

    PR-#39

    ⓘ Recommendations generated based on similar findings in past PRs

    Evidence
    analytics_middleware records ctx.command_name, and Context.command_name is defined as
    interaction.command.name (leaf only). Meanwhile, grouped command registration stores qualified
    names ("{parent.name} {name}") in the registry, indicating leaf names alone are not unique in this
    framework.
    

    easycord/middleware.py[367-388]
    easycord/_context_base.py[56-60]
    easycord/_command_registration.py[163-166]

    Agent prompt
    The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
    
    ## Issue description
    Analytics currently keys counters by the *leaf* command name (`ctx.command_name`). For grouped commands, this can conflate different commands that share the same leaf name under different groups.
    
    ## Issue Context
    - `Context.command_name` returns `interaction.command.name` (leaf name).
    - The framework itself treats grouped commands as qualified names when registering to the registry ("<group> <subcommand>") to avoid collisions.
    
    ## Fix focus areas
    - In `analytics_middleware`, record a qualified name when available (e.g., `interaction.command.qualified_name` if present), falling back to `ctx.command_name`.
    - Alternatively (or additionally), update `Context.command_name` to prefer `qualified_name` over `name` if the attribute exists.
    
    ### Fix Focus Areas
    - easycord/middleware.py[367-388]
    - easycord/_context_base.py[56-60]
    - easycord/_command_registration.py[163-166]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


    Grey Divider

    ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

    Qodo Logo

    Comment thread easycord/bot.py
    Comment on lines +118 to +119
    self._analytics_store: AnalyticsStore | None = None
    self._guild_disabled_plugins: dict[int, set[str]] = {}

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    Action required

    1. Per-guild state stored in-memory 📘 Rule violation ⌂ Architecture

    The PR introduces per-guild feature-flag state and per-guild analytics counters stored on the Bot
    instance (_guild_disabled_plugins, _analytics_store) without any database persistence. This
    violates the requirement that long-lived per-guild state must be persisted in the database rather
    than held in-process on the bot.
    
    Agent Prompt
    ## Issue description
    Per-guild feature flags (`_guild_disabled_plugins`) and per-guild analytics counters (`_analytics_store` / `AnalyticsStore.counts`) are stored only in memory on the `Bot` instance, so they are lost on restart and violate the compliance rule requiring DB-backed per-guild state.
    
    ## Issue Context
    The PR explicitly implements per-guild plugin enable/disable and per-guild command stats. These are durable, guild-scoped data structures keyed by `guild_id`, and therefore must be persisted via the project’s database layer (repository/DAO or GuildRecord-style row), not as a Python dict on the bot.
    
    ## Fix Focus Areas
    - easycord/bot.py[118-119]
    - easycord/_bot_plugins.py[378-408]
    - easycord/_bot_events.py[74-93]
    - easycord/middleware.py[330-388]
    

    ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

    @chatgpt-codex-connector chatgpt-codex-connector 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.

    💡 Codex Review

    Here are some automated review suggestions for this pull request.

    Reviewed commit: 95daab42ba

    ℹ️ About Codex in GitHub

    Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

    • Open a pull request for review
    • Mark a draft as ready
    • Comment "@codex review".

    If Codex has suggestions, it will comment; otherwise it will react with 👍.

    Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

    Comment on lines +113 to +116
    _entry = (
    _registry.slash_commands.get(f"global:{_cmd}")
    or _registry.slash_commands.get(f"guild:{ctx.guild.id}:{_cmd}")
    )

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    P2 Badge Resolve the invoked slash entry before checking flags

    When the invoked command is not a top-level global slash command, this lookup can resolve the wrong registry entry or none at all: grouped slash commands are registered as global:<group> <subcommand> while _cmd is only the subcommand name, and a guild-specific command with the same name as a global command is shadowed because the global key is checked first. In those cases, disabling the plugin for that guild does not reliably block the command that actually ran.

    Useful? React with 👍 / 👎.

    if ephemeral:
    ctx._force_ephemeral = True

    if ctx.guild is not None:

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    P2 Badge Apply plugin flags to context-menu callbacks

    This guard only runs in build_slash_callback, so plugins that expose @user_command or @message_command still execute their context-menu commands after bot.disable_plugin(name, guild_id) for the same guild. Since these are registered plugin commands too, disabling a plugin leaves part of its user-facing command surface active.

    Useful? React with 👍 / 👎.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    dependencies documentation Improvements or additions to documentation enhancement New feature or request tests

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    3 participants