Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Refactor multiple cogs to utilize a new architecture. #12

New issue

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

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

Already on GitHub? Sign in to your account

Open
Zingzy wants to merge 3 commits into main
base: main
Choose a base branch
Loading
from refactor

Conversation

Copy link
Collaborator

@Zingzy Zingzy commented Jun 5, 2025
edited by sourcery-ai bot
Loading

Reducing code reusability and complexity and improving maintainability. Consolidate error handling and embed management across commands, enhancing user feedback and experience. Introduce centralized logging for image generation processes and streamline image request handling with the new ImageRequestBuilder

Description

Brief description of the changes made in this PR.

Type of Change

Please delete options that are not relevant:

  • Code refactoring (no functional changes)

Testing

Please describe the tests that you ran to verify your changes:

  • Tested locally with a test Discord server
  • All commands work as expected
  • Error scenarios and edge cases tested
  • Proper logging output verified
  • Code formatted with uvx ruff format
  • Code linted with uvx ruff check --fix

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Summary by Sourcery

Refactor all core image command cogs to adopt a new extensible architecture: introduce a shared BaseCommandCog for common logic, leverage BaseImageView and command-specific views for UI buttons, and unify image request handling with ImageRequestBuilder. Consolidate error handling and logging, eliminate duplication, and add comprehensive documentation for the new design.

New Features:

  • Introduce ImageRequestBuilder for fluent, centralized construction of image generation requests
  • Add BaseCommandCog to encapsulate common command logic, including error handling and logging
  • Create BaseImageView and specific view subclasses (ImagineView, CrossPollinateView, MultiPollinateView, BeemojiView) for UI components

Enhancements:

  • Refactor all image command cogs to extend BaseCommandCog and use the new view architecture, reducing duplication and complexity
  • Centralize error handling, embed creation, and logging within base classes to ensure consistent behavior
  • Streamline image generation workflows by replacing manual URL construction with the builder pattern

Documentation:

  • Add REFACTORING_GUIDE.md to describe the new architecture, migration steps, and overall refactoring benefits

Chores:

  • Remove obsolete utility functions and duplicated code in individual cogs
  • Update filesystem helper to exclude base_command_cog.py from dynamic loading

Zingzy and others added 3 commits June 4, 2025 18:18
...omplexity and improving maintainability. Consolidate error handling and embed management across commands, enhancing user feedback and experience. Introduce centralized logging for image generation processes and streamline image request handling with the new ImageRequestBuilder.
...e. Update file listing logic in fs.py to exclude base_command_cog.py from Python file filtering, enhancing functionality.
...ation URL for consistency. Modify cross_pollinate_cog.py to include validate_prompt import for enhanced prompt validation.
Copy link

sourcery-ai bot commented Jun 5, 2025
edited
Loading

Reviewer's Guide

This PR refactors the bot’s command cogs and utilities to a new, layered architecture by introducing a BaseCommandCog for shared logic, a unified Views system for UI components, and an ImageRequestBuilder for centralized URL construction, dramatically reducing duplication and improving maintainability.

Sequence Diagram: Refactored Command Execution Flow

sequenceDiagram
 actor User
 participant ImagineCmd as "/pollinate"
 participant ImagineCog as Imagine (Cog)
 participant BaseCog as BaseCommandCog
 participant ImgGenUtils as image_gen_utils
 participant ImgReqBuilder as ImageRequestBuilder
 participant DiscordAPI as Discord API
 participant ExternalImageAPI as Image API
 User->>DiscordAPI: Executes /pollinate command
 DiscordAPI->>ImagineCog: imagine_command(interaction, prompt, ...)
 ImagineCog->>BaseCog: self.log_generation_start(...)
 ImagineCog->>ImgGenUtils: generate_image(prompt, ...)
 activate ImgGenUtils
 ImgGenUtils->>ImgReqBuilder: builder = ImageRequestBuilder.for_standard_generation(prompt)
 ImgGenUtils->>ImgReqBuilder: builder.with_dimensions(...)
 ImgGenUtils->>ImgReqBuilder: builder.build_request_data()
 ImgReqBuilder-->>ImgGenUtils: request_data (incl. URL)
 ImgGenUtils->>ExternalImageAPI: GET request_data.url
 ExternalImageAPI-->>ImgGenUtils: image_data, metadata
 deactivate ImgGenUtils
 ImgGenUtils-->>ImagineCog: (dic, image_file)
 ImagineCog->>BaseCog: self.log_generation_complete(...)
 ImagineCog->>ImagineCog: embed = generate_pollinate_embed(...)
 ImagineCog->>ImagineCog: view = self.get_view_class()()
 ImagineCog->>BaseCog: self.send_response(interaction, embed, file=image_file, view=view)
 BaseCog->>DiscordAPI: Followup response with embed, image, view
 DiscordAPI-->>User: Displays image and buttons
Loading

Sequence Diagram: Refactored 'Edit' Button Interaction

sequenceDiagram
 actor User
 participant ImagineViewInst as ImagineView
 participant EditModal as EditImageModal
 participant ImgGenUtils as image_gen_utils
 participant ImgReqBuilder as ImageRequestBuilder
 participant ExternalImageAPI as Image API
 participant DiscordAPI as Discord API
 User->>DiscordAPI: Clicks "Edit" button
 DiscordAPI->>ImagineViewInst: edit(interaction)
 ImagineViewInst->>DiscordAPI: interaction.response.send_modal(EditModal(...))
 DiscordAPI-->>User: Shows EditImageModal
 User->>DiscordAPI: Submits EditImageModal (with edit_prompt)
 DiscordAPI->>EditModal: on_submit(interaction)
 activate EditModal
 EditModal->>ImgGenUtils: generate_cross_pollinate(prompt=edit_prompt.value, image_url=original_image_url, ...)
 activate ImgGenUtils
 ImgGenUtils->>ImgReqBuilder: builder = ImageRequestBuilder.for_cross_pollination(...)
 ImgGenUtils->>ImgReqBuilder: builder.build_request_data()
 ImgReqBuilder-->>ImgGenUtils: request_data (incl. URL)
 ImgGenUtils->>ExternalImageAPI: GET request_data.url
 ExternalImageAPI-->>ImgGenUtils: edited_image_data, metadata
 deactivate ImgGenUtils
 ImgGenUtils-->>EditModal: (dic, edited_image_file)
 EditModal->>EditModal: embed = generate_cross_pollinate_embed(...)
 EditModal->>DiscordAPI: interaction.followup.send(embed=embed, file=edited_image_file, view=CrossPollinateView())
 deactivate EditModal
 DiscordAPI-->>User: Displays edited image and new buttons
Loading

Class Diagram: Command Cog Hierarchy

classDiagram
 class BaseCommandCog {
 +bot: commands.Bot
 +command_name: str
 +command_config: dict
 +logger: Logger
 +__init__(bot, command_name_in_config)
 +cog_load()*
 +handle_command_error(interaction, error, **context)*
 +send_response(interaction, embed, file, ephemeral, view_instance)*
 +get_view_class() View
 #log_generation_start(model, dimensions, cached, action)
 #log_generation_complete(model, dimensions, generation_time, cached, action)
 }
 class Imagine {
 +__init__(bot)
 +get_view_class() ImagineView
 +imagine_command(interaction, prompt, ...)
 }
 class CrossPollinate {
 +__init__(bot)
 +get_view_class() CrossPollinateView
 +cross_pollinate_command(interaction, prompt, image, ...)
 }
 class Beemoji {
 +__init__(bot)
 +get_view_class() BeemojiView
 +beemoji_command(interaction, emoji1, emoji2, ...)
 }
 class MultiPollinate {
 +__init__(bot)
 +get_view_class() MultiPollinateView
 +multiimagine_command(interaction, prompt, ...)
 }
 class RandomImage {
 +__init__(bot)
 +get_view_class() CrossPollinateView
 +random_image_command(interaction, ...)
 }
 BaseCommandCog <|-- Imagine
 BaseCommandCog <|-- CrossPollinate
 BaseCommandCog <|-- Beemoji
 BaseCommandCog <|-- MultiPollinate
 BaseCommandCog <|-- RandomImage
Loading

Class Diagram: UI View Hierarchy

classDiagram
 class BaseImageView {
 +timeout: Optional[float]
 +__init__(timeout)
 +setup_common_buttons()* 
 +interaction_check(interaction)* bool
 +handle_delete_button(interaction)*
 +handle_bookmark_button(interaction)*
 +get_original_data(interaction)* dict
 +get_prompt_from_embed(embed) str
 #_get_bookmark_type() str
 }
 class ImagineView {
 +setup_buttons()* 
 +regenerate(interaction)*
 +edit(interaction)*
 }
 class CrossPollinateView {
 +setup_buttons()* 
 +edit(interaction)*
 }
 class MultiPollinateView {
 +image_count: int
 +setup_buttons()* 
 +handle_upscale_button(interaction, index)*
 }
 class BeemojiView {
 +emoji1_name: str
 +emoji2_name: str
 +generated_name: str
 +setup_buttons()* 
 +edit(interaction)*
 +add_to_server(interaction)*
 +reduce_image_size(image_data, width, height) bytes
 }
 class EditImageModal {
 +original_image_url: str
 +original_prompt: str
 +edit_prompt: TextInput
 +__init__(original_image_url, original_prompt)
 +on_submit(interaction)*
 }
 BaseImageView <|-- ImagineView
 BaseImageView <|-- CrossPollinateView
 BaseImageView <|-- MultiPollinateView
 BaseImageView <|-- BeemojiView
 ImagineView ..> EditImageModal : "uses"
 CrossPollinateView ..> EditImageModal : "uses"
 BeemojiView ..> EditImageModal : "uses for edit"
Loading

Class Diagram: Image Request Construction

classDiagram
 class ImageRequest {
 +prompt: Optional[str]
 +width: int
 +height: int
 +model: Optional[str]
 +safe: bool
 +cached: bool
 +nologo: bool
 +enhance: bool
 +private: bool
 +negative: Optional[str]
 +seed: Optional[str]
 +image_url: Optional[str]
 +to_dict() dict
 }
 class ImageRequestBuilder {
 -base_url: str
 -request: ImageRequest
 +__init__(base_url)
 +with_prompt(prompt) ImageRequestBuilder
 +with_dimensions(width, height) ImageRequestBuilder
 +with_model(model) ImageRequestBuilder
 +with_source_image(image_url) ImageRequestBuilder
 +build_url() str
 +build_request_data() dict
 +for_standard_generation(prompt, **kwargs)$ ImageRequestBuilder
 +for_cross_pollination(prompt, image_url, **kwargs)$ ImageRequestBuilder
 +for_random_generation(**kwargs)$ ImageRequestBuilder
 }
 class MultiImageRequestBuilder {
 -base_request: ImageRequest
 -models: list[str]
 +__init__(base_request)
 +with_base_prompt(prompt) MultiImageRequestBuilder
 +build_requests() list[ImageRequestBuilder]
 }
 ImageRequestBuilder "1" *-- "1" ImageRequest : aggregates
 MultiImageRequestBuilder "1" *-- "1" ImageRequest : aggregates
 MultiImageRequestBuilder ..> ImageRequestBuilder : creates
Loading

File-Level Changes

Change Details Files
Introduce BaseCommandCog for shared command logic
  • Add BaseCommandCog with standardized error handling, logging, and response methods
  • Refactor all command cogs to extend BaseCommandCog, removing duplicated cooldown/validation and embed logic
  • Replace in-cog error handlers with BaseCommandCog.handle_command_error
  • Adopt log_generation_start/complete and send_response utilities in cogs
cogs/base_command_cog.py
cogs/imagine_cog.py
cogs/cross_pollinate_cog.py
cogs/multi_pollinate_cog.py
cogs/random_cog.py
cogs/beemoji_cog.py
Migrate UI components into a unified Views architecture
  • Create BaseImageView for common delete/bookmark buttons
  • Extract and refactor command-specific buttons and modals into separate view modules
  • Register each view in its cog via get_view_class or add_view
  • Remove inline UI and modal classes from cogs
views/base_view.py
views/imagine_view.py
views/cross_pollinate_view.py
views/multi_pollinate_view.py
views/beemoji_view.py
views/__init__.py
Centralize request URL construction with ImageRequestBuilder
  • Add ImageRequestBuilder and MultiImageRequestBuilder using builder pattern
  • Refactor generate_image, generate_cross_pollinate, generate_random_image to use the builder
  • Remove manual URL concatenation and seed logic from utils and cogs
utils/image_request_builder.py
utils/image_gen_utils.py

Tips and commands

Interacting with Sourcery

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

Customizing Your Experience

Access your dashboard to:

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

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Zingzy - I've reviewed your changes - here's some feedback:

  • BaseCommandCog.send_response ignores file and view when ephemeral=True, so ephemeral responses cannot include attachments or interactive buttons; consider including them for consistent UX.
  • You can DRY up your cog setup by using the provided setup_base_cog helper in each setup function instead of manually calling bot.add_cog and logging.
  • The manual string concatenation in ImageRequestBuilder.build_url can miss edge‐case escaping—consider using urllib.parse.urlencode to assemble query parameters more robustly.
Here's what I looked at during the review
  • 🟡 General issues: 6 issues found
  • 🟢 Security: all looks good
  • 🟢 Testing: all looks good
  • 🟡 Complexity: 3 issues found
  • 🟢 Documentation: all looks good

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

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji

return (request_data, image_file)

except aiohttp.ClientError as e:
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

suggestion (bug_risk): APIError is raised with only the string message, which may lose status code context.

Consider passing the status code to APIError if available, to preserve error context for downstream handlers.

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
else:
await response.edit(embeds=embeds, attachments=files)
# For multi-pollinate, we need to edit the response with the grid
view = MultiPollinateView(image_count=len(embeds))
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

issue: MultiPollinateView is instantiated with image_count, but get_view_class does not pass this parameter.

If get_view_class is used elsewhere without image_count, this could cause inconsistent behavior. Please standardize how MultiPollinateView is instantiated or clearly document the image_count requirement.

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
"""Handle button interactions."""
custom_id = interaction.data["custom_id"]

if custom_id.startswith("U"):
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

issue (bug_risk): Index parsing only handles single-digit custom_ids

Parsing only custom_id[1] fails for multi-digit indices. Use int(custom_id[1:]) to handle all cases correctly.

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
)
return

# Get the original image URL from the message embed
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

issue (bug_risk): Only checks the first embed for an image

Iterate through all embeds to locate the first one with an image, rather than assuming the first embed always contains it.

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji

async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Handle button interactions."""
custom_id = interaction.data["custom_id"]
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

issue (bug_risk): Direct access to interaction.data['custom_id'] may throw KeyError

Use interaction.data.get('custom_id') or check if 'custom_id' exists before accessing it to prevent KeyError.

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
return (request_data, image_file)

except aiohttp.ClientError as e:
raise APIError(f"Network error occurred: {str(e)}")
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
raise APIError(f"Network error occurred: {str(e)}")
raise APIError(f"Network error occurred: {str(e)}")frome

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
return (request_data, image_file)

except aiohttp.ClientError as e:
raise APIError(f"Network error occurred: {str(e)}")
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
raise APIError(f"Network error occurred: {str(e)}")
raise APIError(f"Network error occurred: {str(e)}")frome

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
"Authorization": f"Bearer {config.api.api_key}",
}

try:
Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

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

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
Comment on lines +121 to +124
# Add dimensions
params.append(f"width={self.request.width}")
params.append(f"height={self.request.height}")

Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

suggestion (code-quality): Merge consecutive list appends into a single extend (merge-list-appends-into-extend)

Suggested change
# Add dimensions
params.append(f"width={self.request.width}")
params.append(f"height={self.request.height}")
params.extend((f"width={self.request.width}", f"height={self.request.height}"))

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
Comment on lines +27 to +33
# Find the prompt field (different commands use different field names)
prompt = "Unknown prompt"
for field in interaction_data.get("fields", []):
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]:
prompt = field["value"][3:-3] # Remove ``` code block markers
break

Copy link

@sourcery-ai sourcery-ai bot Jun 5, 2025

Choose a reason for hiding this comment

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

suggestion (code-quality): Use the built-in function next instead of a for-loop (use-next)

Suggested change
# Find the prompt field (different commands use different field names)
prompt = "Unknown prompt"
for field in interaction_data.get("fields", []):
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]:
prompt = field["value"][3:-3] # Remove ``` code block markers
break
prompt = next(
(
field["value"][3:-3]
for field in interaction_data.get("fields", [])
if field["name"] in ["Prompt", "Cross-Pollinate Prompt 🐝"]
),
"Unknown prompt",
)

sourcery-ai[bot] reacted with thumbs up emoji sourcery-ai[bot] reacted with thumbs down emoji
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

1 more reviewer

@sourcery-ai sourcery-ai[bot] sourcery-ai[bot] approved these changes

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /