Skip to content

Support += and -= for multi-value fields in modify#6648

Open
wanxidong wants to merge 7 commits into
beetbox:masterfrom
wanxidong:modify-multivalue-operators
Open

Support += and -= for multi-value fields in modify#6648
wanxidong wants to merge 7 commits into
beetbox:masterfrom
wanxidong:modify-multivalue-operators

Conversation

@wanxidong

Copy link
Copy Markdown

Description

Fixes #6587.

This PR adds += and -= support to beet modify for multi-value fields.

For example:

beet modify genres+=Funk
beet modify genres-=Blues

With this change, genres+=Funk appends Funk to the existing genres values if it is not already present, while genres-=Blues removes only the exact value Blues and preserves other values such as Blues Rock.

The existing field=value replacement behavior is preserved.

Main behavior changes:

  • genres=Jazz; Blues still replaces the full field value as before.
  • genres+=Funk appends Funk to the existing multi-value field.
  • genres+=Funk does not create duplicate values.
  • genres-=Blues removes only the exact value Blues.
  • Removing Blues does not remove partial matches like Blues Rock.
  • Existing value order is preserved.
  • += and -= are rejected for scalar fields with a clear user-facing error.

To Do

  • Documentation. I have not added documentation yet. I can add this to the modify command documentation if maintainers prefer.
  • Changelog. I have not added a changelog entry yet and can add one after review if this change is accepted.
  • Tests. Added tests for multi-value assignment, append, duplicate prevention, exact removal, partial-match preservation, ordering, and scalar-field operator errors.

Verification

Ran:

poetry run pytest test/ui/commands/test_modify.py -q

@wanxidong wanxidong requested a review from a team as a code owner May 17, 2026 10:24
@github-actions

Copy link
Copy Markdown

Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.

@codecov

codecov Bot commented May 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.82%. Comparing base (c90f42d) to head (bd431ef).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6648      +/-   ##
==========================================
+ Coverage   74.78%   74.82%   +0.03%     
==========================================
  Files         163      163              
  Lines       20966    20991      +25     
  Branches     3302     3308       +6     
==========================================
+ Hits        15680    15707      +27     
+ Misses       4529     4528       -1     
+ Partials      757      756       -1     
Files with missing lines Coverage Δ
beets/ui/commands/modify.py 95.65% <100.00%> (+1.62%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@JOJ0

JOJ0 commented May 19, 2026

Copy link
Copy Markdown
Member

I really love the feature idea and the straightforward syntax suggestion but don't really have time to look into it these days....

If I'm not mistaken this feature was requested already, maybe even multiple times. Do you mind looking up those issues or did you already and couldn't find them?

@snejus snejus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well done. I think we'd like ModifyOperation class to be a bit smarter and own all relevant methods, see my comments

Comment thread beets/ui/commands/modify.py Outdated


@dataclass(frozen=True)
class ModifyOperation:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be simplified to a NamedTuple.

Comment thread beets/ui/commands/modify.py Outdated
)


def _apply_modify_operation(obj, field, mod, value):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I want this to be a method on ModifyOperation, e.g. def apply.

Comment thread beets/ui/commands/modify.py Outdated
key, operator = key[:-1], key[-1]
key = maybe_replace_legacy_field(key, is_album, modify=True)
mods[key] = val
mods[key] = ModifyOperation(operator, val) if operator else val

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's only have ModifyOperations here! Simply set operator=None when we have no operator.

@JOJ0

JOJ0 commented Jun 25, 2026

Copy link
Copy Markdown
Member

hi @Victor0700 will you find time to continue on this PR? In case not, I might pick it up and address @snejus suggestions. I can't promise any timeframe though. So what's your status?

Anyway, I'm using these features and can't live out them already 😆 Thanks a ton for the submission!

@JOJ0 JOJ0 force-pushed the modify-multivalue-operators branch from 910fa5f to bc3f456 Compare July 3, 2026 05:22
@JOJ0

JOJ0 commented Jul 3, 2026

Copy link
Copy Markdown
Member

@snejus and @wanxidong I'm taking over here with addressing the suggested code changes. Those are done and ready for a review. Will provide docs later. HTH :-)

@snejus

snejus commented Jul 3, 2026

Copy link
Copy Markdown
Member

@JOJ0 I love your keenness! I actually want these features as well.

from .utils import do_query


class ModifyOperation(NamedTuple):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Each of the new functions and ModifyOperation.apply need types here

Comment on lines +71 to +75
obj_mods[key] = (
mod.apply(obj[key], parsed_value)
if mod.operator is not None
else parsed_value
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ModifyOperation now handles the case when operation is None:

Suggested change
obj_mods[key] = (
mod.apply(obj[key], parsed_value)
if mod.operator is not None
else parsed_value
)
obj_mods[key] = mod.apply(obj[key], parsed_value)

item.load()
assert item.title == f"{orig_title} - append"

def test_modify_multi_value_assignment(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All these tests (except test_modify_scalar_operator_error) should be used with @pytest.mark.parametrize.

Just define a new class TestMultiValue(TestHelper),
define a @pytest.fixture for item and merge them into a single parametrized test. See test_sort.py for some examples.

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.

Addition and subtraction operators for multi-value fields

3 participants