Skip to content

Master#39

Merged
rolling-codes merged 4 commits into
mainfrom
master
May 8, 2026
Merged

Master#39
rolling-codes merged 4 commits into
mainfrom
master

Conversation

@rolling-codes

Copy link
Copy Markdown
Owner

No description provided.

tbirrell0-cell and others added 2 commits May 7, 2026 17:45
…ands

- `@slash(require_admin=True)` shorthand gates commands to guild admins
- `@command_error("cmd")` registers per-command error handlers on Plugin classes
- `describe(**kw)` attaches per-parameter descriptions shown in Discord's command picker
- All three wired through _scan_methods, _build_slash_callback, and _register_slash
- 9 new tests; 420 pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added a centralized InteractionRegistry
- Unified error pipeline for components, modals, autocomplete, and tasks
- Added command sync planning and dynamic component routing
- Added reusable option validators

@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.

Sorry @rolling-codes, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@rolling-codes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 30 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 50f1c629-e7b6-43b5-b847-e28f2996f71a

📥 Commits

Reviewing files that changed from the base of the PR and between b57a2af and 9640e74.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • context/architecture.md
  • easycord/_bot_commands.py
  • easycord/_bot_events.py
  • easycord/_bot_plugins.py
  • easycord/config.py
  • easycord/plugin.py
  • easycord/registry.py
  • easycord/validators.py
  • pyproject.toml
  • tests/test_new_decorators.py
  • tests/test_v52_error_pipeline.py
  • tests/test_v52_interactions.py
📝 Walkthrough

Walkthrough

This pull request bumps EasyCord to v5.2.0 with major feature additions: a centralized interaction registry for command/component/modal/autocomplete tracking, enhanced slash-command guards (require_admin, cooldown with bucket modes, premium_required), a unified error pipeline with plugin-scoped on_error handlers, dynamic component routing with TTL support, config-driven bot startup, testing utilities (FakeContext, invoke helpers), and comprehensive documentation. All changes include extensive test coverage validating new guard behavior, error routing precedence, registry collision detection, and config/sync workflows.

Changes

Core Framework Architecture & v5.2.0 Release

Layer / File(s) Summary
Registry and Validators
easycord/registry.py, easycord/validators.py
InteractionEntry dataclass with typed component routing (pattern/regex/variables), InteractionRegistry expanded with separate buckets for slash/context/component/modal/autocomplete, collision detection for dynamic routes, and TTL expiry support; new validator callables for Duration, URL, Snowflake, Range, Regex, ChoiceSet with user-friendly ValidationError.
Configuration System
easycord/config.py
BotConfig dataclass with environment/file-based loading, field validation (token, db_backend, log_level), precedence rules, and build_bot() constructor forwarding config to Bot instance.
Context Extensions
easycord/_context_base.py
BaseContext gains app_context property, entitlements property, respond()/send() gain silent/suppress_embeds parameters, new forward() method for message forwarding.
Decorator Enhancements
easycord/decorators.py
New decorators: describe() for parameter descriptions, command_error() for command-scoped handlers, updated cooldown() with bucket modes (user/guild/global), require_permissions(), autocomplete() with flexible signatures, updated slash() with require_admin/premium_required metadata, task() with backoff support, component() with ttl parameter.
Plugin Error Handling & Lifecycle
easycord/plugin.py, easycord/_bot_plugins.py
Plugin.on_error() hook for plugin-scoped error handling; task_statuses() method for status snapshots; plugin unload unregisters via registry; _run_task() refactored to track state (running/stopped/failed), record exceptions, dispatch errors, and support restart with backoff.
Command Dispatch Pipeline
easycord/_bot_commands.py
Slash callback builder enforces guards (admin, permissions, premium, cooldown); autocomplete handlers support both (ctx, current, options) and (current) signatures; autocomplete errors dispatch via _dispatch_framework_error(); context menus register with metadata (nsfw, allowed_contexts, allowed_installs).
Event Dispatch & Error Routing
easycord/_bot_events.py
New _dispatch_framework_error() routes exceptions through plugin.on_error then bot.on_error; component dispatch uses registry.resolve_component() for TTL/typed-variable support; modal dispatch wraps errors in exception handler.
Bot Inspection & Sync
easycord/bot.py
Bot.init adds sync_guild_id parameter; inspect_interactions() returns grouped registry inventory; plan_command_sync() diffs local vs remote commands; sync_commands() executes sync with dry_run/confirm_removals; enable_interaction_inspector() registers developer command; setup_hook() performs guild-scoped or global sync.
Testing Utilities
easycord/testing.py
FakeInteraction mocks discord.Interaction; FakeContext extends Context with response capture and assertion helpers (assert_content, was_ephemeral); invoke() executes command callbacks; invoke_autocomplete() executes autocomplete handlers with flexible signatures.
Public API & Version
easycord/__init__.py, pyproject.toml
Version bumped to 5.2.0; new exports: BotConfig, decorators (autocomplete, command_error, cooldown, describe, install_type, premium_required, require_permissions, slash_command), validators (ChoiceSet, Duration, Range, Regex, Snowflake, URL, ValidationError).
Packaging & Metadata
MANIFEST.in, .gitignore, AGENTS.md, CHANGELOG.md, context/architecture.md
MANIFEST.in includes AGENTS.md/CLAUDE.md; .gitignore adds build/pytest artifacts; CHANGELOG.md adds v5.2.0 section; AGENTS.md and context/architecture.md updated with current decorators/registry; README.md links to v5.2.0 release with new feature bullets.
Documentation
docs/command-sync.md, docs/components-dynamic-routing.md, docs/interactions.md, docs/getting-started.md, README.md
New docs for command sync planning, dynamic component routing with typed IDs and TTL, interactions registry and inspector; expanded getting-started with config-driven startup and command guards; README updated with v5.2.0 features and links.
Config & Context Tests
tests/test_config.py, tests/test_context.py
BotConfig.from_env/from_file precedence, build_bot() backend selection, Bot.setup_hook() guild sync; BaseContext.respond/send silent/suppress_embeds flags; Context.forward() message forwarding.
Decorator & Guard Tests
tests/test_new_decorators.py
describe() metadata stacking; command_error() registration; require_admin enforcement; cooldown/permissions/install_type/premium_required metadata and runtime behavior; error handler precedence (command_error → plugin.on_error → bot.on_error); scanner metadata forwarding.
API & Release Tests
tests/test_public_api.py, tests/test_release_readiness.py
Public API smoke tests and private symbol checks; release readiness validating version consistency, MANIFEST assets, documentation URLs, and all symbol resolution.
Testing Framework Tests
tests/test_testing.py
FakeContext.make() identity, response recording, invoke() command execution, plugin command dispatch.
v5.2 Feature Tests
tests/test_v52_error_pipeline.py, tests/test_v52_interactions.py
Component/modal/autocomplete exception routing to plugin.on_error with fallback; task failure state and exception capture; plugin unload safety; interaction registry tracking; component collision detection and TTL expiry; command sync planning and dry-run; validator behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

release:minor

Poem

🐰 A version springs forth, v5.2.0 in sight,
With guards and cool handlers, errors routed right,
Components now dance with typed IDs so free,
And tests catch the bugs before users can see!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch master

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 and usage tips.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

v5.2.0 Release: Command Guards, Interaction Registry, and Testing Infrastructure

✨ Enhancement 🧪 Tests 📝 Documentation

Grey Divider

Walkthroughs

Description
• **Major version release (v5.2.0)** with comprehensive command guards, interaction registry, and
  testing infrastructure
• **New decorator suite**: @describe, @command_error, @cooldown, @require_permissions,
  @premium_required, @install_type, @autocomplete for advanced command metadata and guards
• **Structured interaction registry** (InteractionRegistry) with pattern matching for dynamic
  component routing, TTL expiration, and collision detection
• **Command sync planning**: plan_command_sync() and sync_commands() methods for previewing and
  executing syncs with dry-run and confirmation support
• **Configuration management**: New BotConfig class for environment and file-based bot
  configuration with validation
• **Testing framework**: FakeContext, FakeInteraction, and invoke() helpers for unit testing
  commands without Discord connection
• **Validators module**: Reusable option validators (Duration, URL, Snowflake, Range,
  Regex, ChoiceSet) with user-safe error messaging
• **Enhanced error pipeline**: Unified error routing through per-command, plugin-scoped, and global
  handlers via @command_error and plugin.on_error()
• **Plugin lifecycle improvements**: Task status tracking, restart/backoff support, and
  plugin-scoped error handling
• **Context enhancements**: New app_context, entitlements properties, send() alias,
  forward() method, and silent/suppress_embeds parameters
• **Comprehensive test coverage**: 6 new test modules covering decorators, registry, sync planning,
  config, testing utilities, and release readiness
• **Documentation expansion**: New guides for interactions, dynamic routing, command sync, and
  updated getting-started with v5.2 features
Diagram
flowchart LR
  A["Decorators<br/>@cooldown @require_permissions<br/>@describe @command_error"] -->|metadata| B["InteractionRegistry<br/>Structured inventory<br/>Pattern matching"]
  C["BotConfig<br/>from_env/from_file"] -->|configure| D["Bot<br/>sync_commands<br/>plan_command_sync"]
  D -->|inspect| B
  E["@slash commands"] -->|register| B
  F["Testing<br/>FakeContext<br/>invoke"] -->|test| E
  G["Validators<br/>Duration URL Snowflake"] -->|validate| E
  H["Error Pipeline<br/>@command_error<br/>plugin.on_error"] -->|route| D
  I["Component Routing<br/>Dynamic patterns<br/>TTL expiration"] -->|register| B
Loading

Grey Divider

File Changes

1. tests/test_new_decorators.py 🧪 Tests +587/-0

New decorator and command guard test coverage

• Comprehensive test suite for new decorator features (describe, command_error, install_type)
• Tests for require_admin parameter in slash commands with permission validation
• Tests for stacked decorator metadata propagation (cooldown, permissions, install_type,
 premium_required)
• Tests for command guards (cooldown rates, guild buckets, premium entitlements, permissions)
• Tests for error handling pipeline (command-specific, plugin, and global handlers)

tests/test_new_decorators.py


2. easycord/_bot_commands.py ✨ Enhancement +214/-24

Enhanced slash command guards and error routing

• Added require_admin, cooldown_rate, cooldown_bucket, and premium_required parameters to
 slash() decorator
• Implemented watch_plugins() method for development-time plugin file watching and lifecycle
 reloading
• Enhanced _build_slash_callback() with multi-use cooldown tracking, premium entitlement checks,
 and error handler routing
• Added support for per-command error handlers via _command_error_handlers dictionary
• Improved autocomplete handling with context and options support, plus error routing
• Added registry tracking for slash commands and context menus with metadata

easycord/_bot_commands.py


3. easycord/decorators.py ✨ Enhancement +258/-6

New decorator suite for command guards and metadata

• Added describe() decorator for per-parameter descriptions in slash commands
• Added command_error() decorator to mark plugin methods as command-specific error handlers
• Added install_type() decorator to control guild vs. user app installation contexts
• Added cooldown() decorator with rate, window, and bucket support (user/guild/global)
• Added require_permissions() decorator for permission-based command gating
• Added premium_required decorator for Discord monetization entitlement checks
• Added autocomplete() decorator for standalone autocomplete callbacks
• Enhanced slash() with new parameters and metadata propagation from stacked decorators
• Added slash_command as public alias for slash
• Enhanced task() decorator with restart and backoff parameters
• Enhanced context menu decorators with nsfw, allowed_contexts, and allowed_installs
 parameters
• Enhanced component() decorator with ttl parameter for temporary component handlers

easycord/decorators.py


View more (27)
4. easycord/registry.py ✨ Enhancement +352/-27

Structured interaction registry with pattern matching

• Redesigned InteractionRegistry with InteractionEntry dataclass for structured metadata
 tracking
• Added support for slash commands, context menus, components, modals, and autocomplete callbacks
• Implemented dynamic component pattern matching with typed variables (int, snowflake, str)
• Added collision detection for static and dynamic component IDs
• Added component TTL (time-to-live) expiration tracking
• Added unregister_plugin() for bulk removal of plugin-owned entries
• Added grouped() and iter_syncable() methods for inventory inspection

easycord/registry.py


5. easycord/config.py ✨ Enhancement +232/-0

Configuration management for bot startup

• New BotConfig dataclass for validated environment and file-based configuration
• Supports reading from environment variables with from_env() class method
• Supports reading from JSON config files with from_file() class method
• Validates token, database backend, log level, and guild ID on construction
• Provides build_bot() helper to construct a Bot instance with config values
• Includes get() method for accessing arbitrary extra configuration keys

easycord/config.py


6. easycord/testing.py ✨ Enhancement +262/-0

Testing helpers for command unit tests

• New testing module with FakeInteraction and FakeContext for unit testing commands
• Implements fake responder and followup objects that capture responses for assertions
• Provides invoke() helper to call registered slash commands without Discord connection
• Provides invoke_autocomplete() helper to test autocomplete callbacks
• Supports permission, entitlement, and admin status simulation in tests

easycord/testing.py


7. easycord/bot.py ✨ Enhancement +107/-2

Command sync planning and interaction inspection

• Added sync_guild_id parameter for development guild command syncing
• Added inspect_interactions() method to view registered interaction inventory
• Added plan_command_sync() method to preview sync diffs without contacting Discord
• Added sync_commands() method for explicit command sync with dry-run and confirmation
• Added enable_interaction_inspector() to register optional /easycord interactions diagnostic
 command
• Updated setup_hook() to support guild-scoped command syncing
• Added _command_error_handlers dictionary for per-command error routing
• Added _task_statuses tracking for background task state monitoring

easycord/bot.py


8. tests/test_v52_interactions.py 🧪 Tests +185/-0

Registry, sync planning, and validator tests

• Tests for registry inventory tracking of slash commands and context menus
• Tests for dynamic component route parsing with typed variables
• Tests for component TTL expiration safety
• Tests for plugin unload registry cleanup
• Tests for autocomplete decorator and testing helper
• Tests for command sync planning and dry-run detection
• Tests for task status tracking and failure recording
• Tests for option validators (Duration, URL, Snowflake, Range, Regex, ChoiceSet)

tests/test_v52_interactions.py


9. easycord/_bot_plugins.py ✨ Enhancement +96/-11

Plugin scanning and task lifecycle enhancements

• Enhanced _scan_methods() to collect standalone autocomplete decorators and merge with command
 handlers
• Added support for require_admin, cooldown_rate, cooldown_bucket, and premium_required
 metadata
• Added _command_error_handlers registration for per-command error handlers
• Added context menu metadata propagation (nsfw, allowed_contexts, allowed_installs)
• Added component TTL support in registration
• Enhanced remove_plugin() to unregister all plugin-owned registry entries
• Enhanced reload_plugin() to preserve task state during reload
• Enhanced _start_plugin_tasks() with task status tracking and restart/backoff support
• Enhanced _run_task() with error routing, restart logic, and status updates
• Added task_statuses() method to inspect background task state

easycord/_bot_plugins.py


10. tests/test_v52_error_pipeline.py 🧪 Tests +186/-0

Unified error pipeline routing tests

• Tests for component error routing to plugin on_error handler
• Tests for component error fallback to global error handler
• Tests for modal error routing through error pipeline
• Tests for autocomplete error handling with empty result fallback
• Tests for task error recording and error pipeline routing
• Tests for task cancellation during unload without triggering error handlers

tests/test_v52_error_pipeline.py


11. easycord/_bot_events.py ✨ Enhancement +64/-11

Unified error dispatch and component routing

• Added _dispatch_framework_error() method for unified error routing through plugin and global
 handlers
• Enhanced _register_component_handler() to support TTL parameter
• Enhanced component() decorator to accept TTL parameter
• Enhanced _dispatch_component() to use registry pattern matching and error routing
• Enhanced _dispatch_modal() to use error routing pipeline

easycord/_bot_events.py


12. easycord/_context_base.py ✨ Enhancement +102/-0

Context enhancements for app context and message forwarding

• Added app_context property to access Discord application context (guild/DM/private channel)
• Added entitlements property to access user's active premium entitlements
• Enhanced respond() with silent and suppress_embeds parameters
• Added send() method as compatibility alias for respond()
• Added forward() method to forward messages with fallback for older discord.py versions

easycord/_context_base.py


13. tests/test_config.py 🧪 Tests +131/-0

Configuration loading and validation tests

• Tests for BotConfig.from_env() with environment variable reading and override precedence
• Tests for BotConfig.from_file() with JSON config file parsing and merge precedence
• Tests for validation of token, guild ID, log level, and database backend
• Tests for build_bot() integration with memory and SQLite backends
• Tests for guild-scoped command syncing via sync_guild_id

tests/test_config.py


14. easycord/__init__.py ✨ Enhancement +20/-2

Public API exports for v5.2 features

• Updated version to 5.2.0
• Added exports for new decorators: describe, command_error, cooldown, require_permissions,
 premium_required, autocomplete, install_type, slash_command
• Added exports for BotConfig configuration class
• Added exports for validators: Duration, Range, Regex, Snowflake, URL, ChoiceSet,
 ValidationError

easycord/init.py


15. MANIFEST.in ⚙️ Configuration changes +5/-2

Package manifest updates for documentation

• Added AGENTS.md and CLAUDE.md to included files
• Added recursive includes for docs, examples, and context directories
• Removed exclusions for CLAUDE.md and AGENTS.md

MANIFEST.in


16. easycord/validators.py ✨ Enhancement +115/-0

New validators module for command option validation

• New module providing reusable option validators for command handlers
• Implements ValidationError exception with user-safe messaging and translation support
• Adds validator classes: Duration (parses time strings like "10s", "5m"), URL (validates
 HTTP/HTTPS), Snowflake (validates Discord IDs), Range (numeric bounds), Regex (pattern +
 length), and ChoiceSet (fixed set validation)

easycord/validators.py


17. tests/test_release_readiness.py 🧪 Tests +108/-0

Release readiness and metadata consistency tests

• New test module for release readiness verification
• Validates version consistency across pyproject.toml, easycord.__version__, README, docs, and
 CHANGELOG
• Verifies MANIFEST.in includes required documentation assets and examples
• Checks that builtin plugin documentation matches loader and public API exports resolve correctly

tests/test_release_readiness.py


18. tests/test_context.py 🧪 Tests +55/-0

Context response options and forwarding tests

• Added tests for silent and suppress_embeds parameters in ctx.respond() for both initial and
 followup responses
• Added test for ctx.send() alias that uses the respond flow
• Added test for ctx.forward() fallback behavior with empty messages

tests/test_context.py


19. tests/test_testing.py 🧪 Tests +53/-0

Testing utilities and fake context tests

• New test module for easycord.testing helpers
• Tests FakeContext.make() for creating mock contexts with user/guild IDs
• Tests FakeContext assertions like assert_content(), assert_contains(), and response tracking
• Tests invoke() function for running registered bot and plugin commands without Discord
 connection

tests/test_testing.py


20. tests/test_public_api.py 🧪 Tests +45/-0

Public API export verification tests

• New test module for public package exports
• Verifies that key public APIs (BotConfig, cooldown, describe, etc.) are importable and
 listed in __all__
• Checks that no private modules (names starting with _ but not dunder) are exposed in public API

tests/test_public_api.py


21. easycord/plugin.py ✨ Enhancement +16/-0

Plugin-scoped error handling hook

• Added on_error() async method to Plugin class for plugin-scoped error handling
• Method receives context and exception, runs after per-command @command_error handlers but before
 global bot.on_error
• Includes docstring with example usage for custom error responses

easycord/plugin.py


22. README.md 📝 Documentation +105/-7

Version 5.2.0 release notes and documentation updates

• Updated version from 5.1.1 to 5.2.0 throughout (install links, badges, release links)
• Added comprehensive v5.2.0 release notes covering interaction registry, dynamic routing,
 validators, and task supervision
• Added new sections: "Config-driven startup", "Command guards and context helpers", "Testing
 commands"
• Updated plugin loading documentation to clarify starter set vs. explicit loading
• Changed AI provider reference from detailed section to "AI Orchestration" link

README.md


23. docs/getting-started.md 📝 Documentation +76/-6

Getting started guide for v5.2.0 features

• Updated install link from v5.1.1 to v5.2.0
• Added "Config-driven startup" section with BotConfig.from_env() example
• Updated "Loading bundled plugins" section to clarify starter set (welcome, tags, polls, levels)
 vs. selective loading
• Added "Command guards and responses" section with decorator examples and plugin error handling
• Added "Testing commands" section with FakeContext and invoke() examples

docs/getting-started.md


24. CHANGELOG.md 📝 Documentation +47/-1

Changelog entries for v5.2.0 and v5.1.2 releases

• Added v5.2.0 release notes with highlights on interaction registry, command sync planning, dynamic
 routing, validators, and task supervision
• Added v5.1.2 release notes (2026-05-07) covering bug fixes, config-driven startup, testing
 utilities, and command guards
• Reorganized previous v5.1.1 release notes as "Previous" section

CHANGELOG.md


25. AGENTS.md 📝 Documentation +9/-7

Agent documentation updates for v5.2.0 architecture

• Updated framework version reference from v5.1.0 to v5.1.2
• Added ctx.send() to context API examples
• Expanded decorators list to include @autocomplete, @cooldown, @require_permissions,
 @install_type, @premium_required
• Added new "Interaction registry" layer describing registry.py as authoritative inventory
• Clarified plugin system: starter set via load_builtin_plugins() and explicit loading with
 bot.add_plugin()
• Updated branch/repo state notes with current verification status

AGENTS.md


26. context/architecture.md 📝 Documentation +8/-4

Architecture documentation for v5.2.0 layers

• Updated version reference from v5.1.0 to v5.1.2
• Added ctx.send() to context API examples
• Expanded decorators list with new guards and autocomplete decorator
• Added new "Interaction registry" layer section describing registry.py
• Added "Config and testing" layer section for BotConfig and FakeContext
• Clarified plugin loading: starter set vs. explicit loading with bot.add_plugin()

context/architecture.md


27. docs/interactions.md 📝 Documentation +65/-0

New interactions documentation for v5.2

• New documentation file for v5.2 interaction system
• Explains InteractionRegistry as framework inventory while CommandTree remains Discord sync
 backend
• Documents @slash_command as compatibility alias for @slash
• Describes bot.inspect_interactions() for grouped interaction inspection
• Covers autocomplete callback registration with @autocomplete decorator and testing via
 invoke_autocomplete()

docs/interactions.md


28. pyproject.toml ⚙️ Configuration changes +4/-3

Project metadata and dependencies for v5.2.0

• Updated version from 5.1.1 to 5.2.0
• Added tomli>=2 dependency for Python < 3.11 in dev dependencies (for tomllib fallback)
• Updated download and release URLs to point to v5.2.0 release artifacts

pyproject.toml


29. docs/components-dynamic-routing.md 📝 Documentation +43/-0

New dynamic component routing documentation

• New documentation file for v5.2 dynamic component routing
• Explains typed custom ID patterns with supported types: str, int, snowflake
• Shows examples of single and multi-parameter routes like ticket:close:{ticket_id:int}
• Documents ttl= parameter for temporary in-process routes with expiration
• Notes collision detection for static and dynamic component IDs

docs/components-dynamic-routing.md


30. docs/command-sync.md 📝 Documentation +47/-0

New command sync planning documentation

• New documentation file for v5.2 command sync planning
• Explains bot.plan_command_sync() for previewing changes before syncing
• Documents dry-run sync with bot.sync_commands(dry_run=True)
• Covers confirm_removals=True requirement for destructive syncs
• Shows guild-scoped sync with guild_id= parameter for development iteration

docs/command-sync.md


Grey Divider

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

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. New bot behavior in bot.py 📘 Rule violation ⌂ Architecture
Description
easycord/bot.py adds new bot behavior methods (inspect_interactions, plan_command_sync,
sync_commands, enable_interaction_inspector) directly in bot.py instead of a _bot_*.py
mixin. This violates the project requirement to keep bot.py limited to wiring/composition and can
make bot behavior harder to modularize and maintain.
Code

easycord/bot.py[R125-214]

+    def inspect_interactions(self) -> dict[str, list[dict[str, Any]]]:
+        """Return registered interactions grouped by EasyCord interaction type."""
+        return self.registry.grouped()
+
+    def plan_command_sync(
+        self,
+        *,
+        guild_id: int | None = None,
+        remote_commands: list[str] | None = None,
+    ) -> dict[str, list[str]]:
+        """Build a command sync diff without contacting Discord by default.
+
+        Pass ``remote_commands`` in tests or after your own fetch to compare the
+        current EasyCord inventory with Discord's current command names.
+        """
+        local_entries = self.registry.iter_syncable(guild_id=guild_id)
+        local_names = [entry.name for entry in local_entries if entry.enabled]
+        remote_names = list(remote_commands or [])
+
+        warnings: list[str] = []
+        duplicates = sorted({name for name in local_names if local_names.count(name) > 1})
+        for name in duplicates:
+            warnings.append(f"Duplicate local command name: {name}")
+
+        local_set = set(local_names)
+        remote_set = set(remote_names)
+        return {
+            "added": sorted(local_set - remote_set),
+            "changed": [],
+            "removed": sorted(remote_set - local_set),
+            "unchanged": sorted(local_set & remote_set),
+            "warnings": warnings,
+        }
+
+    async def sync_commands(
+        self,
+        *,
+        guild_id: int | None = None,
+        dry_run: bool = False,
+        remote_commands: list[str] | None = None,
+        confirm_removals: bool = False,
+    ) -> dict[str, list[str]]:
+        """Plan or execute command sync through ``discord.py``'s CommandTree.
+
+        If the plan detects removals, pass ``confirm_removals=True`` before a
+        non-dry-run sync. This keeps EasyCord from applying a destructive sync
+        without an explicit caller decision.
+        """
+        plan = self.plan_command_sync(
+            guild_id=guild_id,
+            remote_commands=remote_commands,
+        )
+        if dry_run:
+            return plan
+        if plan["removed"] and not confirm_removals:
+            raise RuntimeError(
+                "Command sync would remove remote commands. Re-run with "
+                "confirm_removals=True after reviewing plan_command_sync()."
+            )
+        guild = discord.Object(id=guild_id) if guild_id is not None else None
+        if guild is not None:
+            self.tree.copy_global_to(guild=guild)
+            await self.tree.sync(guild=guild)
+        else:
+            await self.tree.sync()
+        for entry in self.registry.iter_syncable(guild_id=guild_id):
+            entry.sync_state = "synced"
+        return plan
+
+    def enable_interaction_inspector(self, *, owner_ids: set[int] | None = None) -> None:
+        """Register the optional ``/easycord interactions`` developer command."""
+        group = app_commands.Group(
+            name="easycord",
+            description="EasyCord developer diagnostics",
+        )
+
+        @group.command(name="interactions", description="Show registered EasyCord interactions")
+        async def interactions(interaction: discord.Interaction) -> None:
+            if owner_ids is not None and interaction.user.id not in owner_ids:
+                await interaction.response.send_message("Not authorized.", ephemeral=True)
+                return
+            grouped = self.inspect_interactions()
+            lines = [
+                f"{kind}: {len(entries)}"
+                for kind, entries in grouped.items()
+            ]
+            await interaction.response.send_message("\n".join(lines), ephemeral=True)
+
+        self.tree.add_command(group)
+
Evidence
Compliance ID 526288 requires new bot-level behavior to be implemented in _bot_*.py mixins rather
than directly in bot.py. The diff shows multiple new concrete behavior methods added directly to
easycord/bot.py.

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

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New bot behavior was added directly to `easycord/bot.py`, but project policy requires bot behavior to live in `_bot_*.py` mixin modules, keeping `bot.py` as thin wiring/composition.
## Issue Context
The PR adds interaction inspection and command sync planning/execution methods directly on the `Bot` class in `bot.py`.
## Fix Focus Areas
- easycord/bot.py[125-214]

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


2. Tests import easycord._bot_* 📘 Rule violation ⌂ Architecture
Description
tests/test_new_decorators.py imports internal easycord._bot_* modules from outside the
easycord/ package. This violates the rule that external code (including tests outside the package)
must only use EasyCord’s public API surface.
Code

tests/test_new_decorators.py[97]

+        from easycord._bot_plugins import _PluginsMixin
Evidence
Compliance ID 526296 disallows importing underscored/internal EasyCord modules from outside the
easycord/ package. The changed test file imports _PluginsMixin and _CommandsMixin directly
from easycord._bot_plugins/easycord._bot_commands.

Rule 526296: Only import public easycord API from outside the package
tests/test_new_decorators.py[97-97]
tests/test_new_decorators.py[127-127]
tests/test_new_decorators.py[183-183]
tests/test_new_decorators.py[534-534]
tests/test_new_decorators.py[553-553]

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 located outside the `easycord/` package import internal underscored modules (e.g., `easycord._bot_commands`, `easycord._bot_plugins`). External code must only use the public API from `easycord`.
## Issue Context
These tests currently reach into internal mixins to test scanning/building behavior.
## Fix Focus Areas
- tests/test_new_decorators.py[97-97]
- tests/test_new_decorators.py[127-127]
- tests/test_new_decorators.py[183-183]
- tests/test_new_decorators.py[534-534]
- tests/test_new_decorators.py[553-553]

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


3. Leaking command error handlers 🐞 Bug ☼ Reliability
Description
_scan_methods() registers per-command error handlers into _command_error_handlers, but
remove_plugin() never unregisters them, leaving stale bound methods after plugin unload. If another
plugin later registers a command with the same name, failures can be routed to the stale handler
(wrong behavior or crash).
Code

easycord/_bot_plugins.py[R87-88]

+            if getattr(method, "_is_command_error", False):
+                self._command_error_handlers[method._command_error_for] = method
Evidence
Handlers are added to a global dict keyed only by command name, and plugin unload does not remove
those entries; slash execution consults this dict on exception.

easycord/_bot_plugins.py[23-90]
easycord/_bot_plugins.py[211-274]
easycord/_bot_commands.py[274-283]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_command_error_handlers` is populated during plugin scanning, but plugin removal does not clean up entries. This can leave stale handlers that will be invoked for future commands with the same name.
### Issue Context
- Registration happens in `_scan_methods()`.
- Lookup happens in `_build_slash_callback()` when a command raises.
- `remove_plugin()` should remove any handlers owned by that plugin.
### Fix Focus Areas
- easycord/_bot_plugins.py[23-274]
- easycord/_bot_commands.py[274-296]
### Suggested fix
- In `remove_plugin()`, delete any `_command_error_handlers` entries whose value is a bound method on `plugin` (e.g., `getattr(handler, "__self__", None) is plugin`).
- Optionally, store handlers in a structure keyed by plugin name to make cleanup deterministic.

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


View more (2)
4. Autocomplete registry key bug 🐞 Bug ≡ Correctness
Description
Autocomplete registry entries are keyed using an already-scoped name in register_autocomplete(), and
_register_named() prepends the scope again; additionally, slash registration passes only the
unqualified command name (not the group-qualified name). This combination can raise ValueError
collisions for common setups like two SlashGroups both having a subcommand named "kick" with
autocomplete.
Code

easycord/registry.py[R189-195]

+        key = f"{self._scope_key(guild_id)}:{command_name}:{option_name}"
+        return self._register_named(
+            self.autocomplete_callbacks,
+            "autocomplete",
+            key,
+            func,
+            source_plugin=source_plugin,
Evidence
register_autocomplete() builds key with _scope_key() and then calls _register_named(), which
prefixes _scope_key() again. Separately, _register_slash() computes a qualified registry_name
but does not use it for autocomplete registration, so grouped subcommands sharing names collide.

easycord/registry.py[179-216]
easycord/_bot_commands.py[339-396]
easycord/_bot_commands.py[401-402]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Autocomplete registrations can collide because:
1) `InteractionRegistry.register_autocomplete()` passes a *scoped* string into `_register_named()`, which scopes again.
2) `_register_slash()` registers autocomplete using the unqualified command name (`name`) rather than the group-qualified `registry_name`.
### Issue Context
This causes collisions for SlashGroup subcommands that share a name across groups (a normal pattern), and results in confusing stored `InteractionEntry.name` values.
### Fix Focus Areas
- easycord/registry.py[179-231]
- easycord/_bot_commands.py[339-419]
### Suggested fix
- Change `InteractionRegistry.register_autocomplete()` to pass an *unscoped* name into `_register_named()`, e.g. `name = f"{command_name}:{option_name}"` and let `_register_named()` add the scope.
- In `_register_slash()`, use the computed `registry_name` (includes parent group when present) when calling `registry.register_autocomplete(...)` and in the stored metadata, so grouped commands are uniquely identified.

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


5. Handler exceptions escape 🐞 Bug ☼ Reliability
Description
If a per-command error handler or Plugin.on_error raises, that exception is not caught inside
_build_slash_callback() and can bubble out of the interaction callback. Because build_chain() does
not catch exceptions, this can fail the interaction without invoking global on_error or logging via
the framework dispatcher.
Code

easycord/_bot_commands.py[R281-292]

+                    if per_cmd is not None:
+                        await per_cmd(ctx, exc)
+                        return
+                    plugin = getattr(func, "__self__", None)
+                    if plugin is not None:
+                        from .plugin import Plugin as _Plugin
+                        if isinstance(plugin, _Plugin):
+                            plugin_on_error = type(plugin).on_error
+                            base_on_error = _Plugin.on_error
+                            if plugin_on_error is not base_on_error:
+                                await plugin.on_error(ctx, exc)
+                                return
Evidence
_build_slash_callback() awaits per_cmd(...) / plugin.on_error(...) directly without a protective
try/except; the middleware chain simply composes calls and does not provide a catch-all exception
handler.

easycord/_bot_commands.py[274-296]
easycord/middleware.py[28-37]
easycord/_bot_events.py[100-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Exceptions thrown *inside* per-command error handlers or `Plugin.on_error()` are not handled, so they can escape the interaction callback.
### Issue Context
- `build_chain()` does not catch exceptions; it only wraps middleware.
- The framework already has `_dispatch_framework_error()` which safely wraps plugin/global handlers.
### Fix Focus Areas
- easycord/_bot_commands.py[274-296]
- easycord/middleware.py[28-37]
- easycord/_bot_events.py[100-134]
### Suggested fix
- Wrap `await per_cmd(ctx, exc)` and `await plugin.on_error(ctx, exc)` in `try/except Exception as handler_exc`.
- On handler failure, log with `logger.exception(...)` and then fall through to the next handler (plugin → global) or call `_dispatch_framework_error(handler_exc, ctx=ctx, plugin_instance=plugin)` (careful to avoid recursion).
- Similarly consider guarding `self._error_handler(ctx, exc)` for parity with `_dispatch_framework_error()`.

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



Remediation recommended

6. Cooldown memory growth 🐞 Bug ➹ Performance
Description
Cooldown tracking keeps all timestamps within the window in a Python list per bucket, making each
invocation O(n) due to filtering and allowing large in-memory growth for guild/global buckets under
load. This can degrade latency and increase memory usage for popular commands.
Code

easycord/_bot_commands.py[R256-265]

+                    used_at = [
+                        ts
+                        for ts in _cooldown_last_used.get(bucket_key, [])
+                        if now - ts < cooldown
+                    ]
+                    if len(used_at) >= cooldown_rate:
+                        remaining = cooldown - (now - used_at[0])
                     await ctx.respond(
                         ctx.t(
                             "errors.cooldown",
Evidence
The code filters and stores a list of all timestamps still inside the cooldown window; with high
traffic and longer windows, the list can become large and is re-scanned every call.

easycord/_bot_commands.py[248-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Cooldown bookkeeping is list-based and scans the entire list every invocation; it also retains every hit inside the window.
### Issue Context
The algorithm only needs the last `cooldown_rate` timestamps per bucket to decide whether to allow the next call.
### Fix Focus Areas
- easycord/_bot_commands.py[248-273]
### Suggested fix
- Replace `list[float]` with `collections.deque[float]` per bucket.
- On each call: pop left while `now - dq[0] >= cooldown`; if `len(dq) >= cooldown_rate` block; else append `now`.
- This bounds memory and makes per-call work proportional to expired entries, not total traffic.

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


7. Component errors lose source ✓ Resolved 🐞 Bug ☼ Reliability
Description
In _dispatch_component(), when the handler is resolved via the legacy "suffix" path (registered_id
ending with "_"), plugin_name is derived from the unresolved entry (None). As a result,
_dispatch_framework_error() cannot locate the owning plugin and Plugin.on_error is skipped for those
component handlers.
Code

easycord/_bot_events.py[R267-268]

+                plugin_name = entry.get("source") if entry else None
+                await self._dispatch_framework_error(exc, ctx=ctx, plugin_name=plugin_name)
Evidence
The suffix fallback can select handler from candidate_entry without updating entry, but
plugin_name is computed only from entry. _dispatch_framework_error relies on plugin_name to find
the plugin instance.

easycord/_bot_events.py[242-251]
easycord/_bot_events.py[266-268]
easycord/_bot_events.py[100-124]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When using the underscore-suffix component routing fallback, the selected `candidate_entry` is not used to derive `plugin_name`, so plugin-scoped error handling is skipped.
### Issue Context
`_dispatch_framework_error()` can route to `Plugin.on_error` if it knows the plugin name/instance.
### Fix Focus Areas
- easycord/_bot_events.py[235-270]
### Suggested fix
- In the suffix fallback loop, when a match is found, also set `entry = candidate_entry` (or separately track `plugin_name = candidate_entry.get("source")`).
- Optionally, honor TTL/disabled entries by ensuring the fallback only uses active entries (consistent with `registry.resolve_component()`).

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


Grey Divider

Qodo Logo

Comment thread easycord/bot.py
Comment on lines +125 to +214
def inspect_interactions(self) -> dict[str, list[dict[str, Any]]]:
"""Return registered interactions grouped by EasyCord interaction type."""
return self.registry.grouped()

def plan_command_sync(
self,
*,
guild_id: int | None = None,
remote_commands: list[str] | None = None,
) -> dict[str, list[str]]:
"""Build a command sync diff without contacting Discord by default.

Pass ``remote_commands`` in tests or after your own fetch to compare the
current EasyCord inventory with Discord's current command names.
"""
local_entries = self.registry.iter_syncable(guild_id=guild_id)
local_names = [entry.name for entry in local_entries if entry.enabled]
remote_names = list(remote_commands or [])

warnings: list[str] = []
duplicates = sorted({name for name in local_names if local_names.count(name) > 1})
for name in duplicates:
warnings.append(f"Duplicate local command name: {name}")

local_set = set(local_names)
remote_set = set(remote_names)
return {
"added": sorted(local_set - remote_set),
"changed": [],
"removed": sorted(remote_set - local_set),
"unchanged": sorted(local_set & remote_set),
"warnings": warnings,
}

async def sync_commands(
self,
*,
guild_id: int | None = None,
dry_run: bool = False,
remote_commands: list[str] | None = None,
confirm_removals: bool = False,
) -> dict[str, list[str]]:
"""Plan or execute command sync through ``discord.py``'s CommandTree.

If the plan detects removals, pass ``confirm_removals=True`` before a
non-dry-run sync. This keeps EasyCord from applying a destructive sync
without an explicit caller decision.
"""
plan = self.plan_command_sync(
guild_id=guild_id,
remote_commands=remote_commands,
)
if dry_run:
return plan
if plan["removed"] and not confirm_removals:
raise RuntimeError(
"Command sync would remove remote commands. Re-run with "
"confirm_removals=True after reviewing plan_command_sync()."
)
guild = discord.Object(id=guild_id) if guild_id is not None else None
if guild is not None:
self.tree.copy_global_to(guild=guild)
await self.tree.sync(guild=guild)
else:
await self.tree.sync()
for entry in self.registry.iter_syncable(guild_id=guild_id):
entry.sync_state = "synced"
return plan

def enable_interaction_inspector(self, *, owner_ids: set[int] | None = None) -> None:
"""Register the optional ``/easycord interactions`` developer command."""
group = app_commands.Group(
name="easycord",
description="EasyCord developer diagnostics",
)

@group.command(name="interactions", description="Show registered EasyCord interactions")
async def interactions(interaction: discord.Interaction) -> None:
if owner_ids is not None and interaction.user.id not in owner_ids:
await interaction.response.send_message("Not authorized.", ephemeral=True)
return
grouped = self.inspect_interactions()
lines = [
f"{kind}: {len(entries)}"
for kind, entries in grouped.items()
]
await interaction.response.send_message("\n".join(lines), ephemeral=True)

self.tree.add_command(group)

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. New bot behavior in bot.py 📘 Rule violation ⌂ Architecture

easycord/bot.py adds new bot behavior methods (inspect_interactions, plan_command_sync,
sync_commands, enable_interaction_inspector) directly in bot.py instead of a _bot_*.py
mixin. This violates the project requirement to keep bot.py limited to wiring/composition and can
make bot behavior harder to modularize and maintain.
Agent Prompt
## Issue description
New bot behavior was added directly to `easycord/bot.py`, but project policy requires bot behavior to live in `_bot_*.py` mixin modules, keeping `bot.py` as thin wiring/composition.

## Issue Context
The PR adds interaction inspection and command sync planning/execution methods directly on the `Bot` class in `bot.py`.

## Fix Focus Areas
- easycord/bot.py[125-214]

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

bot._register_component_handler = MagicMock()
bot._register_modal_handler = MagicMock()

from easycord._bot_plugins import _PluginsMixin

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

2. Tests import easycord.bot* 📘 Rule violation ⌂ Architecture

tests/test_new_decorators.py imports internal easycord._bot_* modules from outside the
easycord/ package. This violates the rule that external code (including tests outside the package)
must only use EasyCord’s public API surface.
Agent Prompt
## Issue description
Tests located outside the `easycord/` package import internal underscored modules (e.g., `easycord._bot_commands`, `easycord._bot_plugins`). External code must only use the public API from `easycord`.

## Issue Context
These tests currently reach into internal mixins to test scanning/building behavior.

## Fix Focus Areas
- tests/test_new_decorators.py[97-97]
- tests/test_new_decorators.py[127-127]
- tests/test_new_decorators.py[183-183]
- tests/test_new_decorators.py[534-534]
- tests/test_new_decorators.py[553-553]

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

Comment thread easycord/_bot_plugins.py
Comment on lines +87 to +88
if getattr(method, "_is_command_error", False):
self._command_error_handlers[method._command_error_for] = method

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

3. Leaking command error handlers 🐞 Bug ☼ Reliability

_scan_methods() registers per-command error handlers into _command_error_handlers, but
remove_plugin() never unregisters them, leaving stale bound methods after plugin unload. If another
plugin later registers a command with the same name, failures can be routed to the stale handler
(wrong behavior or crash).
Agent Prompt
### Issue description
`_command_error_handlers` is populated during plugin scanning, but plugin removal does not clean up entries. This can leave stale handlers that will be invoked for future commands with the same name.

### Issue Context
- Registration happens in `_scan_methods()`.
- Lookup happens in `_build_slash_callback()` when a command raises.
- `remove_plugin()` should remove any handlers owned by that plugin.

### Fix Focus Areas
- easycord/_bot_plugins.py[23-274]
- easycord/_bot_commands.py[274-296]

### Suggested fix
- In `remove_plugin()`, delete any `_command_error_handlers` entries whose value is a bound method on `plugin` (e.g., `getattr(handler, "__self__", None) is plugin`).
- Optionally, store handlers in a structure keyed by plugin name to make cleanup deterministic.

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

Comment thread easycord/registry.py
Comment on lines +189 to +195
key = f"{self._scope_key(guild_id)}:{command_name}:{option_name}"
return self._register_named(
self.autocomplete_callbacks,
"autocomplete",
key,
func,
source_plugin=source_plugin,

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

4. Autocomplete registry key bug 🐞 Bug ≡ Correctness

Autocomplete registry entries are keyed using an already-scoped name in register_autocomplete(), and
_register_named() prepends the scope again; additionally, slash registration passes only the
unqualified command name (not the group-qualified name). This combination can raise ValueError
collisions for common setups like two SlashGroups both having a subcommand named "kick" with
autocomplete.
Agent Prompt
### Issue description
Autocomplete registrations can collide because:
1) `InteractionRegistry.register_autocomplete()` passes a *scoped* string into `_register_named()`, which scopes again.
2) `_register_slash()` registers autocomplete using the unqualified command name (`name`) rather than the group-qualified `registry_name`.

### Issue Context
This causes collisions for SlashGroup subcommands that share a name across groups (a normal pattern), and results in confusing stored `InteractionEntry.name` values.

### Fix Focus Areas
- easycord/registry.py[179-231]
- easycord/_bot_commands.py[339-419]

### Suggested fix
- Change `InteractionRegistry.register_autocomplete()` to pass an *unscoped* name into `_register_named()`, e.g. `name = f"{command_name}:{option_name}"` and let `_register_named()` add the scope.
- In `_register_slash()`, use the computed `registry_name` (includes parent group when present) when calling `registry.register_autocomplete(...)` and in the stored metadata, so grouped commands are uniquely identified.

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

Comment thread easycord/_bot_commands.py
Comment on lines +281 to +292
if per_cmd is not None:
await per_cmd(ctx, exc)
return
plugin = getattr(func, "__self__", None)
if plugin is not None:
from .plugin import Plugin as _Plugin
if isinstance(plugin, _Plugin):
plugin_on_error = type(plugin).on_error
base_on_error = _Plugin.on_error
if plugin_on_error is not base_on_error:
await plugin.on_error(ctx, exc)
return

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

5. Handler exceptions escape 🐞 Bug ☼ Reliability

If a per-command error handler or Plugin.on_error raises, that exception is not caught inside
_build_slash_callback() and can bubble out of the interaction callback. Because build_chain() does
not catch exceptions, this can fail the interaction without invoking global on_error or logging via
the framework dispatcher.
Agent Prompt
### Issue description
Exceptions thrown *inside* per-command error handlers or `Plugin.on_error()` are not handled, so they can escape the interaction callback.

### Issue Context
- `build_chain()` does not catch exceptions; it only wraps middleware.
- The framework already has `_dispatch_framework_error()` which safely wraps plugin/global handlers.

### Fix Focus Areas
- easycord/_bot_commands.py[274-296]
- easycord/middleware.py[28-37]
- easycord/_bot_events.py[100-134]

### Suggested fix
- Wrap `await per_cmd(ctx, exc)` and `await plugin.on_error(ctx, exc)` in `try/except Exception as handler_exc`.
- On handler failure, log with `logger.exception(...)` and then fall through to the next handler (plugin → global) or call `_dispatch_framework_error(handler_exc, ctx=ctx, plugin_instance=plugin)` (careful to avoid recursion).
- Similarly consider guarding `self._error_handler(ctx, exc)` for parity with `_dispatch_framework_error()`.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
easycord/_bot_events.py (1)

247-251: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve entry when using legacy component-prefix fallback.

If fallback matching hits (Line 247-Line 251), entry is never assigned, so Line 267 sends plugin_name=None and skips plugin-scoped on_error.

💡 Suggested fix
         if handler is None:
             for registered_id, candidate_entry in self.registry.components.items():  # type: ignore[attr-defined]
                 if registered_id.endswith("_") and custom_id.startswith(registered_id):
+                    entry = candidate_entry
                     handler = candidate_entry["func"]
                     suffix = custom_id[len(registered_id):]
                     break

Also applies to: 267-268

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/_bot_events.py` around lines 247 - 251, The legacy component-prefix
fallback loop over self.registry.components sets handler and suffix but never
assigns entry, so later code sees plugin_name=None and skips plugin-scoped
on_error; update the loop (the block handling registered_id, candidate_entry) to
also set entry = candidate_entry (and derive plugin_name from entry, e.g.
entry.get("plugin_name") or the equivalent key used elsewhere) whenever you pick
the handler/suffix so subsequent error-handling uses the correct plugin-scoped
entry and plugin_name.
easycord/_bot_plugins.py (1)

138-165: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require explicit ToolSafety on AI tools.

Falling back to ToolSafety.SAFE silently accepts tools whose decorator omitted a safety annotation, which weakens the permission model. Reject registration when _ai_tool_safety is missing instead of defaulting it.

As per coding guidelines, "@ai_tool decorator must register a function into ToolRegistry and requires explicit ToolSafety permission annotation`."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/_bot_plugins.py` around lines 138 - 165, The code currently defaults
missing _ai_tool_safety to ToolSafety.SAFE; instead, ensure every decorated AI
tool has an explicit safety annotation by checking for the presence of the
attribute (use hasattr(method, "_ai_tool_safety") or similar) and reject
registration when it's absent (raise a clear exception or skip registration and
log an error) before populating self.ai_tools and calling
self.tool_registry.register; update the logic around the ai_tools dict, the
safety local variable, and the call to tool_registry.register to enforce this
check so no tool is accepted without an explicit ToolSafety.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 29: Update the AGENTS.md note to reflect the actual mixin split: state
that the context implementation is split across _context_base.py,
_context_channels.py, _context_moderation.py, and _context_ui.py and that edits
should target the appropriate mixin rather than editing context.py directly;
explicitly add _context_base.py to the list and reword the sentence that
currently implies context.py is the primary edit target so it instead directs
contributors to the named mixin files (use the exact filenames _context_base.py,
_context_channels.py, _context_moderation.py, _context_ui.py in the sentence).

In `@CHANGELOG.md`:
- Around line 5-20: Add the required blank line after each heading in the new
v5.2.0 changelog section so markdownlint MD022 is satisfied: insert a single
empty line immediately below the "### Highlights" and "### Bug Fixes" headings
and also below the subsection headings inside that section (e.g., any secondary
headings around lines referencing v5.2.0) so the pattern matches the existing
sections; update the CHANGELOG.md accordingly ensuring there is exactly one
blank line after each heading.

In `@context/architecture.md`:
- Around line 13-14: There is a stale, duplicated description of registry.py
that conflicts with the new "Interaction registry" entry; remove or merge the
old Line 19 entry so the file is described only once as the authoritative
InteractionRegistry for slash commands, context menus, components, modals, and
autocomplete callbacks. Edit the architecture.md section containing registry.py
so it either deletes the old "**Registry** — registry.py tracks live slash
commands, component handlers, and modal handlers. Commands accessible at
bot.registry.commands." line, or consolidate it into the new "Interaction
registry" paragraph by keeping the broader InteractionRegistry wording and
optionally preserving any additional accurate details (e.g.,
bot.registry.commands) beneath the single authoritative registry entry
referencing registry.py / InteractionRegistry.

In `@easycord/_bot_commands.py`:
- Around line 365-396: The autocomplete wrapper (_make_autocomplete inside the
loop over autocomplete_handlers) must stop using a blanket TypeError to choose
which handler arity to call; instead inspect the callable signature of handler
(_h) once (using inspect.signature or inspect.getfullargspec) and dispatch
explicitly to the correct call form (e.g., bound method signatures like (self,
ctx, current, options) or (ctx, current, options) vs (ctx, current) vs (current)
etc.), preserving the existing Context creation (Context(interaction)), options
retrieval (self._autocomplete_options(interaction)), and error handling via
self._dispatch_framework_error; remove the inner try/except that treats any
TypeError as arity mismatch and use the inspected parameter list to decide
whether to call _h(ctx, current, options), _h(ctx, current), or _h(current)
before converting results to app_commands.Choice and registering via
cmd.autocomplete and self.registry.register_autocomplete.

In `@easycord/_bot_plugins.py`:
- Around line 87-88: remove_plugin() currently doesn't remove entries that
_scan_methods() registered into _command_error_handlers, ai_tools, and
tool_registry, leaving bound methods that retain references to the plugin
instance; update remove_plugin()/unload logic to iterate those collections
(_command_error_handlers, ai_tools, tool_registry) and remove any entries whose
bound method object's __self__ (or attribute identifying the bound instance) is
the plugin instance being removed, so all handlers/tools registered by that
plugin are cleared and the plugin can be garbage-collected.
- Around line 29-30: The code keys plugin ownership by type(plugin).__name__
(see add_plugin and usages of plugin_name and standalone_autocomplete), which
collapses multiple instances of the same class; change to an instance-stable
identifier instead: generate or use a per-instance id (e.g., plugin_id =
getattr(plugin, "instance_id", None) or assign plugin._instance_id = uuid4()
when the plugin is first added) and replace all uses of plugin_name (and any
places building registries or task-status namespaces at the sites you noted: the
add_plugin logic and the other occurrences around the standalone_autocomplete
and registry code) with this plugin_id so each plugin instance has its own
registry entries and task-status namespace.

In `@easycord/bot.py`:
- Around line 129-157: plan_command_sync currently only diffs guild-scoped
entries when guild_id is provided, causing false diffs; update plan_command_sync
to include global commands alongside guild-specific ones by calling
registry.iter_syncable with guild_id=None (or otherwise fetching global entries)
and merging those entries with the guild entries when guild_id is set, ensuring
duplicates are handled and warnings still produced; ensure the returned plan
treats both global and guild-specific names as present (so
added/removed/unchanged reflect the merged set) and mirror this same merge
behavior in the other affected locations (the similar blocks around lines
159-192 and 248-254) so that sync_commands and setup_hook behavior matches the
dry-run plan and global commands are considered "synced" for guild-scoped plans.

In `@easycord/config.py`:
- Around line 143-149: The current override handling incorrectly treats string
values like "false" as True because it simply does auto_sync = bool(raw_sync);
update the logic around raw_sync/auto_sync so that when
overrides.pop("auto_sync", None) returns a str you parse it (e.g., lower() and
compare against ("0","false","no")) to produce a boolean, and only fall back to
bool(raw_sync) for non-string types; change the branch that sets auto_sync
(referencing raw_sync and auto_sync in this block in easycord/config.py)
accordingly so string overrides are handled explicitly.

In `@easycord/decorators.py`:
- Around line 83-90: The decorator currently instantiates
discord.AppInstallationType and discord.AppCommandContext (symbols:
AppInstallationType, AppCommandContext, assigns to func._slash_allowed_installs
and func._slash_allowed_contexts) which require discord.py>=2.4.0; to fix,
either raise the package minimum to discord.py>=2.4.0 in pyproject.toml, or add
a runtime guard in easycord/decorators.py that checks discord.__version__ (or
uses hasattr(discord, "AppInstallationType")) and only constructs
AppInstallationType/AppCommandContext when available, otherwise populate
func._slash_allowed_installs and func._slash_allowed_contexts with safe
fallbacks (e.g., None or a compatibility object) so older discord.py (2.0–2.3.x)
does not raise ImportError/AttributeError.

In `@easycord/registry.py`:
- Around line 258-283: _detect_component_collision currently only compares
existing.regex.pattern strings, so dynamic routes with identical segment
structure but different variable names (e.g., ticket:{id:int} vs
ticket:{other:int}) slip through; update the check to compare parsed segment
sequences instead of raw regexes: add or use the same parser that produces a
list of literal/type tokens for an InteractionEntry (e.g., a segments or tokens
property created when the entry is built), then in _detect_component_collision
compare those token sequences for equality (and for conflicting fullmatches) to
detect collisions regardless of parameter names; keep existing error paths but
change the pattern-equality branch to use the segment/token comparison and raise
the same ValueError including entry.name and existing.name when collisions are
detected so resolve_component cannot silently prefer the earlier registration.

In `@easycord/validators.py`:
- Around line 100-103: The validator's __call__ currently sorts self.choices
which can raise TypeError for mixed-type items and crash validation; change the
formatting logic to avoid direct comparison by sorting using a stable key like
str (e.g., use sorted(self.choices, key=str)) or fall back to an unsorted
iteration if sorting fails, then join those stringified choices into allowed and
raise ValidationError with that message in the same __call__ method.

In `@tests/test_new_decorators.py`:
- Around line 117-123: The test test_slash_default_require_admin_false uses an
undefined symbol reset_cmd in its assertion causing F821; update the assertion
to reference the decorated function ping instead (check
ping._slash_require_admin) so the test asserts ping._slash_require_admin is
False; ensure the unique symbols mentioned are
test_slash_default_require_admin_false, reset_cmd (remove/replace), ping, and
_slash_require_admin.

---

Outside diff comments:
In `@easycord/_bot_events.py`:
- Around line 247-251: The legacy component-prefix fallback loop over
self.registry.components sets handler and suffix but never assigns entry, so
later code sees plugin_name=None and skips plugin-scoped on_error; update the
loop (the block handling registered_id, candidate_entry) to also set entry =
candidate_entry (and derive plugin_name from entry, e.g.
entry.get("plugin_name") or the equivalent key used elsewhere) whenever you pick
the handler/suffix so subsequent error-handling uses the correct plugin-scoped
entry and plugin_name.

In `@easycord/_bot_plugins.py`:
- Around line 138-165: The code currently defaults missing _ai_tool_safety to
ToolSafety.SAFE; instead, ensure every decorated AI tool has an explicit safety
annotation by checking for the presence of the attribute (use hasattr(method,
"_ai_tool_safety") or similar) and reject registration when it's absent (raise a
clear exception or skip registration and log an error) before populating
self.ai_tools and calling self.tool_registry.register; update the logic around
the ai_tools dict, the safety local variable, and the call to
tool_registry.register to enforce this check so no tool is accepted without an
explicit ToolSafety.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1711b516-462b-4322-8db0-06524a67ff81

📥 Commits

Reviewing files that changed from the base of the PR and between 68c954b and b57a2af.

📒 Files selected for processing (31)
  • .gitignore
  • AGENTS.md
  • CHANGELOG.md
  • MANIFEST.in
  • README.md
  • context/architecture.md
  • docs/command-sync.md
  • docs/components-dynamic-routing.md
  • docs/getting-started.md
  • docs/interactions.md
  • easycord/__init__.py
  • easycord/_bot_commands.py
  • easycord/_bot_events.py
  • easycord/_bot_plugins.py
  • easycord/_context_base.py
  • easycord/bot.py
  • easycord/config.py
  • easycord/decorators.py
  • easycord/plugin.py
  • easycord/registry.py
  • easycord/testing.py
  • easycord/validators.py
  • pyproject.toml
  • tests/test_config.py
  • tests/test_context.py
  • tests/test_new_decorators.py
  • tests/test_public_api.py
  • tests/test_release_readiness.py
  • tests/test_testing.py
  • tests/test_v52_error_pipeline.py
  • tests/test_v52_interactions.py

Comment thread AGENTS.md
**Bot core** — `bot.py` defines the `Bot` class via multiple inheritance: `discord.Client` + four mixins (`_bot_commands.py`, `_bot_events.py`, `_bot_guild.py`, `_bot_plugins.py`). Adding bot-level behavior means adding to one of these mixin files.

**Context** — `context.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py`. This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send_embed()`, etc.).
**Context** — `context.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py`. This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send()`, `ctx.send_embed()`, etc.).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the context architecture note with the actual mixin split.

Line 29 should include _context_base.py in the context stack and avoid implying context.py is the primary edit target.

Suggested doc correction
-**Context** — `context.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py`. This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send()`, `ctx.send_embed()`, etc.).
+**Context** — `_context_base.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py` (with `context.py` as the composition entrypoint). This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send()`, `ctx.send_embed()`, etc.).

As per coding guidelines, "**/*_context*.py: Context implementation should be split across _context_base.py, _context_channels.py, _context_moderation.py, and _context_ui.py mixin files — edit the appropriate mixin, not context.py directly."

📝 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
**Context**`context.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py`. This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send()`, `ctx.send_embed()`, etc.).
**Context**`_context_base.py` + `_context_channels.py`, `_context_moderation.py`, `_context_ui.py` (with `context.py` as the composition entrypoint). This is the user-facing API inside command handlers (`ctx.respond()`, `ctx.send()`, `ctx.send_embed()`, etc.).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 29, Update the AGENTS.md note to reflect the actual mixin
split: state that the context implementation is split across _context_base.py,
_context_channels.py, _context_moderation.py, and _context_ui.py and that edits
should target the appropriate mixin rather than editing context.py directly;
explicitly add _context_base.py to the list and reword the sentence that
currently implies context.py is the primary edit target so it instead directs
contributors to the named mixin files (use the exact filenames _context_base.py,
_context_channels.py, _context_moderation.py, _context_ui.py in the sentence).

Comment thread CHANGELOG.md
Comment thread context/architecture.md
Comment thread easycord/_bot_commands.py
Comment thread easycord/_bot_plugins.py Outdated
Comment thread easycord/config.py
Comment thread easycord/decorators.py
Comment thread easycord/registry.py
Comment thread easycord/validators.py
Comment thread tests/test_new_decorators.py
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