Skip to content
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

Rewrite the fish plugin from scratch #5359

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open

Conversation

bal-e
Copy link
Member

@bal-e bal-e commented Jul 11, 2024

This plugin needed significant reworking. It now uses StringIO to construct the completion script and pathlib to manage paths uniformly. Functions now have type annotations and the flow of information through the entire script is much more readable.

More importantly, the actual contents of the script produced have been rewritten entirely. A more sophisticated parsing function is now used to determine when to show global option completions. Based on Fish's source code, a much more comprehensive string escaping function is now used to put data in the completion script. The completion for metadata values (as provided by --extravalues) actually works now: it needed to provide completions for the word the user was in the middle of typing (e.g. artist:ab -> artist:abba), rather than the word following.

  • Documentation
  • Changelog
  • Tests

Copy link

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.

@bal-e
Copy link
Member Author

bal-e commented Jul 11, 2024

Fish's complete builtin offers a -C flag which lets you test what completions would be offered for a certain command-line; this could be used to perform automated testing. I suppose this is doable, by making any such tests conditional on a Fish executable being found. But I don't know whether that should be a separate PR or not. This one is already a complete rewrite of the plugin.

@bal-e
Copy link
Member Author

bal-e commented Jul 11, 2024

cc @Serene-Arc, this follows on from the small changes I made to fish.py in #5337. The more I looked at the code, the more I wanted to rewrite it.

Copy link
Contributor

@Serene-Arc Serene-Arc left a comment

Choose a reason for hiding this comment

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

I'm not familiar with fish but the code looks fine to me. One thing that seems absolutely paramount is tests though. If I can't understand everything that is being done, I'd advocate for tests normally but here it's paramount.

When I look at this, a programmatically generated shell script that is meant to be autoloaded, that screams 'attack vector' to me. At the very least, we need tests that take the generated script, write it to a file, and then the test compares that written script to one we have a string in the document. That will make it crystal clear to developers (and users, if they are inclined to look) what exactly all this code generates, and what changes with each option. Plus, if someone changes the logic, they'll need to change the tests, which will show exactly what will change without any obfuscation.

@bal-e
Copy link
Member Author

bal-e commented Jul 18, 2024

I agree, a comprehensive testing solution is important. I was also concerned about the security of these sorts of shell scripts, but I hadn't thought about just comparing the results to a fixed script string in the tests -- I'll definitely implement that.

@bal-e
Copy link
Member Author

bal-e commented Jul 22, 2024

@Serene-Arc, I've implemented a basic file test, so there is some guarantee that the generated script will not contain malware. I can cover some more cases (e.g. using sample items from the library and checking the script with --extravalues) here, but I'd like to address them and an actual testing solution (where a Fish instance is launched and the generated script is tested) in a later PR. Let me know what you'd prefer.

@bal-e
Copy link
Member Author

bal-e commented Jul 27, 2024

@snejus, I know you were working on the testing infrastructure in #5362. I've ended up getting started on the pytest migration by defining some shared fixtures here, based on the code from your PR -- can you take a look (at beets/test/fixtures.py in particular) to make sure it all checks out? It can coexist with the existing testing infrastructure so we can migrate over time.

snejus
snejus previously approved these changes Jul 28, 2024
@snejus
Copy link
Member

snejus commented Jul 28, 2024

Sorry - my CLI has gone haywire and accidentally approved this PR 😅

@snejus snejus dismissed their stale review July 28, 2024 18:25

Accidental

@bal-e
Copy link
Member Author

bal-e commented Jul 28, 2024

no worries @snejus!

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.

Thanks for starting the pytest setup!!! I left a couple of comments there. Will review the stuff in beetsplug/fish.py in a bit.

beets/test/fixtures.py Show resolved Hide resolved
test/plugins/test_fish.py Outdated Show resolved Hide resolved
test/plugins/test_fish.py Outdated Show resolved Hide resolved
beets/test/fixtures.py Show resolved Hide resolved
beets/test/fixtures.py Outdated Show resolved Hide resolved
beets/test/fixtures.py Outdated Show resolved Hide resolved
beets/test/fixtures.py Outdated Show resolved Hide resolved
beets/test/fixtures.py Outdated Show resolved Hide resolved
beets/test/fixtures.py Outdated Show resolved Hide resolved
@bal-e
Copy link
Member Author

bal-e commented Aug 19, 2024

@snejus bump on review?

@snejus
Copy link
Member

snejus commented Aug 19, 2024

Ah sorry, completely missed out on this! Will have a look now!

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.

Leaving a comment regarding beets config setup getting out of sync between unittest and pytest.

Will have a look at the actual fish plugin changes later today!

beets/test/fixtures.py Show resolved Hide resolved
beets/test/fixtures.py Show resolved Hide resolved
The directory for the user's Fish configuration.
"""

config_home: Path
Copy link
Member

Choose a reason for hiding this comment

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

Consider using platformdirs

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, shoot. I completely forgot about the platformdirs PR I was supposed to open. Do you mind if I leave this as-is for now and address it in that PR?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good. You may be able to drop this altogether if you decide to print the completions to stdout instead of saving them to a file 😉

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.

That's a very clean rewrite! Added a couple of small comments, and a suggestion to print the completions to stdout.

beetsplug/fish.py Outdated Show resolved Hide resolved
beetsplug/fish.py Outdated Show resolved Hide resolved
beetsplug/fish.py Outdated Show resolved Hide resolved
beetsplug/fish.py Show resolved Hide resolved
@bal-e bal-e force-pushed the rewrite-fish branch 2 times, most recently from 79d248f to 9e0c504 Compare September 4, 2024 13:38
@bal-e
Copy link
Member Author

bal-e commented Sep 4, 2024

It looks like the Windows builds are broken because of the reflink issue. @snejus, feel free to review anyway.

@bal-e bal-e requested a review from snejus September 8, 2024 11:26
@snejus
Copy link
Member

snejus commented Sep 12, 2024

It looks like the Windows builds are broken because of the reflink issue. @snejus, feel free to review anyway.

You can now rebase as the fix has been merged upstream 🙂

@@ -0,0 +1,71 @@

# An 'argparse' description for global options for 'beet'.
function __fish_beet_global_optspec
Copy link
Member

Choose a reason for hiding this comment

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

I was meant to add a comment to ask to move this to a .fish file but forgot. Happy to see that it was done nevertheless!

@snejus
Copy link
Member

snejus commented Sep 17, 2024

Everything looks good - there's only one bit that's left, see #5359 (review). What did we decide to do about the test setup?

@bal-e
Copy link
Member Author

bal-e commented Sep 19, 2024

I'll implement it now, @snejus!

@bal-e
Copy link
Member Author

bal-e commented Oct 12, 2024

There seems to be some weird issue with test coverage. @snejus, any idea what's going on?

@snejus
Copy link
Member

snejus commented Oct 13, 2024

There seems to be some weird issue with test coverage. @snejus, any idea what's going on?

I have a feeling that's because the workflow is being run from your fork, where the environmental variable doesn't exist.

Try replacing pull_request with pull_request_target in .github/workflows/ci.yaml?

image

@snejus
Copy link
Member

snejus commented Oct 27, 2024

@bal-e sorry I didn't realise this was ready! If you rebase the PR off master the coverage should run fine now.

Arav K. added 14 commits November 12, 2024 17:50
This plugin needed significant reworking.  It now uses 'StringIO' to
construct the completion script and 'pathlib' to manage paths uniformly.
Functions now have type annotations and the flow of information through
the entire script is much more readable.

More importantly, the actual contents of the script produced have been
rewritten entirely.  A more sophisticated parsing function is now used
to determine when to show global option completions.  Based on Fish's
source code, a much more comprehensive string escaping function is now
used to put data in the completion script.  The completion for metadata
values (as provided by --extravalues) actually works now: it needed to
provide completions for the word the user was in the middle of typing
(e.g. 'artist:ab' -> 'artist:abba'), rather than the word following.
This allows us to test that the generated script is exactly the same as
a pre-set file.
This feature was added in 3.9; instead, we fall back to constructing a
dict from an iterable.
Apparently the 'Iterator' in 'collections.abc' did not support generics,
at least not in 3.8.
- Don't explicitly state 'scope="function"'.
- Use 'tmp_path_factory' instead of doing it manually.
- Set up 'conftest.py' for importing fixtures automatically.
- Fixed a 'mypy' error regarding 'set'

- Print an error if unnecessary arguments are added
@bal-e
Copy link
Member Author

bal-e commented Nov 12, 2024

@snejus I tried pull_request_target, but it looks like that disabled CI checking for the PR entirely. Is there any good way to disable specifically the upload-coverage action for PRs?

@snejus
Copy link
Member

snejus commented Nov 13, 2024

@bal-e see #5479 where I did the same change which later got reverted since I discovered it indeed disabled PR checks 😅

Is there any good way to disable specifically the upload-coverage action for PRs?

Haven't yet looked into this... For now, I guess, you can ignore this failure

@snejus
Copy link
Member

snejus commented Nov 13, 2024

For the future PRs, note that since you're a member of the organization you can push your work directly to a branch in this repo (beetbox/beets) - this issue does not come up. That's what I've been doing myself.

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.

Nice, looks good. A couple of small comments but we're close to completing this!

@@ -1,6 +1,6 @@
name: Test
on:
pull_request:
pull_request_target:
Copy link
Member

Choose a reason for hiding this comment

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

Best to remove this, see my comment for more context

self.io.restore()
self.lib._close()
while len(self.contexts) != 0:
Copy link
Member

Choose a reason for hiding this comment

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

TestCase has a method addCleanup which accepts the finalizer function. It can be called in the very beginning, so it should slightly simplify things here. See

    def setup_with_context_manager(self, cm):
        val = cm.__enter__()
        self.addCleanup(cm.__exit__, None, None, None)
        return val

Interestingly, using addCleanup is recommended over teardown methods, since this makes sure that the cleanup is always performed. On the other hand teardown may not get called if something fails in the setUp method, I think

Copy link
Member

Choose a reason for hiding this comment

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

I think this should remove the need for the contexts list.

db_loc: Path | None = None,
) -> Iterator[Library]:
# Create the Beets library object.
db_loc = util.bytestring_path(db_loc) if db_loc is not None else ":memory:"
Copy link
Member

Choose a reason for hiding this comment

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

mypy is failing here because db_loc type becomes bytes | str while Path | None is expected. You can deal with this by moving this logic directly to the Library call:

    lib = Library(util.bytestring_path(db_loc) if db_loc is not None else ":memory:", str(lib_dir))

Copy link
Member

Choose a reason for hiding this comment

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

Note that bytestring_path cast may not be required since the default value for path parameter seems to be a string:

class Library(dbcore.Database):
    """A database of music containing songs and albums."""

    _models = (Item, Album)

    def __init__(
        self,
        path="library.blb",
        directory: str | None = None,
        path_formats=((PF_KEY_DEFAULT, "$artist/$album/$track $title"),),
        replacements=None,
    ):

# Set up the basic configuration with a HOME directory.
self.create_temp_dir()
temp_dir = Path(os.fsdecode(self.temp_dir))
self.libdir = temp_dir / "libdir"
Copy link
Member

Choose a reason for hiding this comment

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

self.libdir starts as a Path and later gets cast to bytes. As a Path, it is only used within this method, so you may want to assign a local variable for it, and set self.libdir with bytes later:

    libdir = temp_dir / "libdir"
    ...
    self.libdir = util.bytestring_path(libdir)

Note the usage of bytestring_path: remember the weird magic/bytes/Windows logic? 😅

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.

3 participants