Skip to content

Conversation

JOJ0
Copy link
Member

@JOJ0 JOJ0 commented Sep 17, 2025

Description

Shorten final album/genre-apply code. This was longish code already and since the --pretend option is here it is even longer, thus I'd like to smarten/shorten that. Also behavior is changed so that --pretend is automatically enabling force mode (-p or -f is what the user usually expects to being the opposite of each other, but we can discuss that of course)

To Do

  • Documentation. Not required. No behavioral changes
  • Changelog.
  • Test refactored to pytest

Copy link
Contributor

sourcery-ai bot commented Sep 17, 2025

Reviewer's Guide

Refactors genre assignment and pretend-mode handling by extracting common logging/apply logic into a decorator and dedicated methods, updating command and import hooks to use them, auto-enabling force on pretend mode, and streamlining tests with pytest-mock.

File-Level Changes

Change Details Files
Extract logging and pretend-mode branching into a decorator
  • Define log_and_pretend decorator
  • Wrap apply functions to log messages
  • Check config["pretend"] and prefix logs with “Pretend:”
  • Skip actual apply when in pretend mode
beetsplug/lastgenre/__init__.py
Consolidate genre application into two dedicated methods
  • Create _apply_album_genre with decorated logging/pretend support
  • Create _apply_item_genre similarly
  • Move genre assignment and store logic into these methods
beetsplug/lastgenre/__init__.py
Use new apply methods in commands() and imported() hooks and auto-enable force on pretend
  • Replace inline genre assignment in lastgenre_func with calls to apply* methods
  • In commands(), set config["force"] when opts.pretend
  • Update imported() hook to call apply* for albums and items
beetsplug/lastgenre/__init__.py
Introduce pretend configuration default and update changelog
  • Add "pretend": False to default config
  • Record behavior changes in docs/changelog.rst
beetsplug/lastgenre/__init__.py
docs/changelog.rst
Revise pretend-mode tests and add pytest-mock dependency
  • Remove old pretend-mode test
  • Add new pytest-mock based test for pretend behavior
  • Add pytest-mock to pyproject.toml dependencies
test/plugins/test_lastgenre.py
pyproject.toml

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

This comment was marked as resolved.

@JOJ0 JOJ0 changed the title lastgenre: Refactor final genre apply and --pretend option lastgenre: Refactor final genre apply, --pretend option, item to album genre fallback Sep 17, 2025
@JOJ0 JOJ0 marked this pull request as ready for review September 17, 2025 06:12
@JOJ0 JOJ0 requested a review from a team as a code owner September 17, 2025 06:12
@Copilot Copilot AI review requested due to automatic review settings September 17, 2025 06:12

This comment was marked as resolved.

Copilot

This comment was marked as outdated.

Copy link
Contributor

@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 there - I've reviewed your changes - here's some feedback:

  • The fallback-to-album genre behavior is currently hard-coded; consider adding a config option to enable or disable track-level fallback for more flexibility.
  • In the track processing loop you check if item.genre: (the old value) instead of testing the lookup result (item_genre), which can cause incorrect logging or assignment—update that condition to use item_genre.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The fallback-to-album genre behavior is currently hard-coded; consider adding a config option to enable or disable track-level fallback for more flexibility.
- In the track processing loop you check `if item.genre:` (the old value) instead of testing the lookup result (`item_genre`), which can cause incorrect logging or assignment—update that condition to use `item_genre`.

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.

@JOJ0
Copy link
Member Author

JOJ0 commented Sep 17, 2025

Setting this to "ready to review" since I'd like to get this merged before any new feature I have in draft already and to get a sourcery review already..

@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch 3 times, most recently from 6adab6b to 097591e Compare September 17, 2025 07:50
@JOJ0

This comment was marked as outdated.

@JOJ0
Copy link
Member Author

JOJ0 commented Sep 17, 2025

@sourcery-ai guide

@sourcery-ai summary

@snejus
Copy link
Member

snejus commented Sep 17, 2025

I think it would be a good idea to add --pretend flag in a separate PR, and we already have a PR open: #6008. Would you be happy to adjust this PR to not add the --pretend flag and keep it focused on the refactor?

@arsaboo
Copy link
Contributor

arsaboo commented Sep 17, 2025

@snejus if it's not too much of inconvenience, I would rather have @JOJ0 make changes to last genre. They are much closer to it and have other features planned. I'm happy to close the other PR.

@JOJ0

This comment was marked as duplicate.

@JOJ0
Copy link
Member Author

JOJ0 commented Sep 17, 2025

@snejus the refactor does not really make sense without the pretend. it's easier this way.

i would like to close @arsaboo 's PR

what i can do here is remove the track to album fallback feature.

would that be ok with you @snejus ?

Ah let me reconsider @snejus: I see you already reviewed the pretend PR from @arsaboo, so if you think this is ready to merge soon, please go ahead you both and finishe it!

I can work from there an adapt this PR to only make pretend conditional smarter/shorter with my decorator refactor idea here!

I think I misunderstood earlier and you ment it that way @snejus right?

@JOJ0 JOJ0 marked this pull request as draft September 18, 2025 04:57
@JOJ0
Copy link
Member Author

JOJ0 commented Sep 18, 2025

@snejus the refactor does not really make sense without the pretend. it's easier this way.
i would like to close @arsaboo 's PR
what i can do here is remove the track to album fallback feature.
would that be ok with you @snejus ?

Ah let me reconsider @snejus: I see you already reviewed the pretend PR from @arsaboo, so if you think this is ready to merge soon, please go ahead you both and finishe it!

I can work from there an adapt this PR to only make pretend conditional smarter/shorter with my decorator refactor idea here!

I think I misunderstood earlier and you ment it that way @snejus right?

@snejus ? Was this your intention?

@snejus
Copy link
Member

snejus commented Sep 18, 2025

@JOJ0 indeed - it's mostly finished so it's we can finish it there and you can base your work on top of it!

@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from 097591e to d7a3a1e Compare September 21, 2025 06:35
@JOJ0 JOJ0 marked this pull request as ready for review September 21, 2025 06:41

This comment was marked as resolved.

sourcery-ai[bot]

This comment was marked as outdated.

@JOJ0 JOJ0 changed the title lastgenre: Refactor final genre apply, --pretend option, item to album genre fallback lastgenre: Refactor final genre apply and item to album genre fallback Sep 21, 2025
@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from d7a3a1e to 5b2cf75 Compare September 23, 2025 03:52
@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from 5b2cf75 to d6da174 Compare September 25, 2025 04:15
@JOJ0 JOJ0 changed the title lastgenre: Refactor final genre apply and item to album genre fallback lastgenre: Refactor genre applying and pretend mode Sep 25, 2025
JOJ0 added a commit to JOJ0/beets that referenced this pull request Sep 25, 2025
JOJ0 added a commit to JOJ0/beets that referenced this pull request Sep 25, 2025
@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from c92ca25 to a9f68b9 Compare September 25, 2025 05:33
@JOJ0
Copy link
Member Author

JOJ0 commented Sep 25, 2025

@sourcery-ai dismiss

@JOJ0 JOJ0 marked this pull request as draft September 25, 2025 05:46
JOJ0 added a commit to JOJ0/beets that referenced this pull request Sep 25, 2025
@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch 2 times, most recently from dd61c89 to b76cd5b Compare September 25, 2025 08:19
@JOJ0 JOJ0 marked this pull request as ready for review September 25, 2025 08:34
Copy link
Contributor

@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 there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `beetsplug/lastgenre/__init__.py:591` </location>
<code_context>
+                                    item,
+                                )
+                            else:
+                                self._apply_item_genre(item, label, item.genre)
+                                if write:
+                                    item.try_write()
</code_context>

<issue_to_address>
**issue (bug_risk):** Passing item.genre instead of item_genre may result in incorrect genre assignment.

Pass item_genre to _apply_item_genre to ensure the genre field is updated correctly.
</issue_to_address>

### Comment 2
<location> `test/plugins/test_lastgenre.py:175-184` </location>
<code_context>
+def test_pretend_option_skips_library_updates(mocker):
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding test coverage for pretend mode with both album and singleton (track) operations.

Please add a test for pretend mode with singleton tracks to verify correct behavior and logging.
</issue_to_address>

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

@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from b76cd5b to 93610e3 Compare September 25, 2025 08:37
@JOJ0
Copy link
Member Author

JOJ0 commented Sep 25, 2025

  • Consider adding a configuration flag to enable or disable the automatic fallback to album genre, so users can opt out of inherited genres if desired.

The feature was removed from this PR.

  • Add unit tests specifically for the fallback-to-album-genre path to ensure tracks only inherit album genres under the correct conditions.

Removed this feature from this PR. Just refactoring of genre applying/pretend handling now.

  • You might rename the label parameter to something like source for clearer alignment with the existing genre-source terminology.

Terminology changed to "logging label / stage label" a while ago already. This is fine and consistent with other parts of the code.

- Move item and genre apply to separate helper functions. Have one
  function for each to not overcomplicate implementation!
- Use a decorator log_and_pretend that logs and does the right thing
  depending on wheter --pretend was passed or not.
- Sets --force (internally) automatically if --pretend is given (this is
  a behavirol change needing discussion)
only one arg is passed to the info log anymore.
@JOJ0 JOJ0 force-pushed the lastgenre_pretend branch from 93610e3 to aaa9be1 Compare September 25, 2025 19:39
@JOJ0
Copy link
Member Author

JOJ0 commented Sep 25, 2025

@semohr or @snejus I pulled out the test of the unittest class to a self-contained pytest and installed pytest-mock. I didn't like the cascaded patch.object statements. Test is similar in length now (altough self-contained) but I think a bit easier to read. What do you think?

Copy link
Member

@snejus snejus left a comment

Choose a reason for hiding this comment

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

Added a comment regarding making the test simpler and a couple of suggestions how to deduplicate item and album logic further

assert res == ["ambient", "electronic"]


def test_pretend_option_skips_library_updates(mocker):
Copy link
Member

Choose a reason for hiding this comment

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

This test can be simplified and made more clear by:

  1. Keeping the test within the LastGenrePluginTest to remove the manual need to run setUp and tearDown (which negates pyetst benefits).

  2. Using our self.run_command abstraction to run the command. Note you will need to inherit from PluginTestCase and define plugin attribute for this:

    from beets.test.helper import PluginTestCase
    from beetsplug import lastgenre
    
    
    class LastGenrePluginTest(PluginTestCase):
        plugin = "lastgenre"
  3. Using idiomatic self.assertLogs() to catch logging output.

  4. Patching Item.store with a function that 'explodes' with a clear message. Note that I think we're supposed to test store rather than write, I think?

    @patch("beets.ui.should_write", Mock(return_value=True))
    @patch(
        "beetsplug.lastgenre.LastGenrePlugin._get_genre",
        Mock(return_value=("Mock Genre", "mock stage")),
    )
    def test_pretend_option_skips_library_updates(self):
        item = self.create_item(
            album="Pretend Album",
            albumartist="Pretend Artist",
            artist="Pretend Artist",
            title="Pretend Track",
            genre="Original Genre",
        )
        album = self.lib.add_album([item])

        def unexpected_store(*_, **__):
            raise AssertionError("Unexpected store call")

        # Verify that try_write was never called (file operations skipped)
        with (
            patch("beetsplug.lastgenre.Item.store", unexpected_store),
            self.assertLogs() as logs,
        ):
            self.run_command("lastgenre", "--pretend")

        assert "Mock Genre" in str(logs.output)
        album.load()
        assert album.genre == "Original Genre"
        assert album.items()[0].genre == "Original Genre"

write = ui.should_write()
pretend = getattr(opts, "pretend", False)
self.config.set_args(opts)
if opts.pretend:
Copy link
Member

Choose a reason for hiding this comment

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

What is it for?

item_genre, label = self._get_genre(item)

if not item_genre:
self._log.info(
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we have the same logic in two other places?

  1. the singleton track on line 599
  2. the equivalent operation for album tracks under imported, line 617

if write and not pretend:
item.try_write()
singleton_genre, label = self._get_genre(item)
self._apply_item_genre(item, label, singleton_genre)
Copy link
Member

Choose a reason for hiding this comment

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

Why are we not writing singleton like we write album items?

# Store the album genre and inherit to tracks.
else:
album.store()
item_genre, label = self._get_genre(item)
Copy link
Member

Choose a reason for hiding this comment

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

To ensure that album tracks and singletons are processed consistently regardless of whether it's run by the command or the import, I'd suggest defining

    def _process_item(self, item: Item, write: bool = False):
        genre, label = self._get_genre(item)

        if genre:
            self._apply_item_genre(item, label, genre)
            if write and not self.config["pretend"]:
                item.try_write()
        else:
            self._log.info('No genre found for track "{.title}"', item)

Comment on lines +592 to +593
if write:
item.try_write()
Copy link
Member

Choose a reason for hiding this comment

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

Seems like files will get written even if --pretend is given?

Maybe it should be

Suggested change
if write:
item.try_write()
if write and self.config["pretend"]:
item.try_write()

'genre for album "{0.album}" ({1}): {0.genre}', album, src
)
album_genre, label = self._get_genre(album)
self._apply_album_genre(album, label, album_genre)
Copy link
Member

Choose a reason for hiding this comment

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

Seems like album logic is the same in lastgenre_func and imported, so you can probably define

    def _process_album(self, album: Album, write: bool = False):
        album_genre, label = self._get_genre(album)
        self._apply_album_genre(album, label, album_genre)

        # If we're using track-level sources, store the album genre only (this
        # happened in _apply_album_genre already), then also look up individual
        # track genres.
        if "track" in self.sources:
            for item in album.items():
                self._process_item(item, write=write)

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

Successfully merging this pull request may close these issues.

4 participants