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!



@pytest.fixture
def config(
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit concerned about multiple sources of truth for the same beets config setup in setup_beets and this fixture. If these two configurations get out of sync, we are more likely to see unexpected test failures attempting to port tests from unittest to pytest. This can be a nightmare to debug due to the lack of clear indication of the root cause.

What would you think about defining the logic in your config fixture as a context manager in beets.test.helper? This way we could reuse it in both BeetsTestCase and pytest fixtures, and remove the duplicate implementation from TestHelper.setup_beets.

For example

# beets/test/helper.py

@contextmanager
def setup_config(directory: Path):
    config = beets.config
    config.clear()
    config.read()

    config["plugins"] = []
    config["verbose"] = 1
    config["ui"]["color"] = False
    config["threaded"] = False
    config["directory"] = str(tmp_path)

    yield config

    self.config.clear()
    beets.config.read(user=False, defaults=True)

It encapsulates all setup and cleanup logic and it can be used both in the setUp code and pytest fixtures:

# beets/test/helper.py
class TestHelper:
    ...
    def setup_with_context_manager(self, cm):
        val = cm.__enter__()
        self.addCleanup(cm.__exit__, None, None, None)
        return val

    def setUp(self):
        ...
        self.config = self.setup_with_context_manager(setup_config(self.tmp_path))
        ...


# conftest.py
from beets.test.helper import setup_config

@pytest.fixture
def config(tmp_path):
    with setup_config(tmp_path) as config:
        yield config

This way we keep a single source of truth for the beets config setup, maintaining consistency regardless of the test runner. This pattern is also applicable to the rest of the setup (lib, importer etc.). What do you think?

A nice side effect of this approach is that we can use conftest.py to define our fixtures since the stuff useful to people is defined in beets.test.helper and not in the test case itself. This makes the test cases more readable and easier to maintain.

Copy link
Member Author

@bal-e bal-e Aug 20, 2024

Choose a reason for hiding this comment

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

I like this plan, thanks! Once we're done with unittest we can revert back to the existing layout, though. I'll leave a TODO in the code for that future change.

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!

Arav K. added 4 commits September 19, 2024 21:23
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.
Arav K. added 10 commits September 19, 2024 21:23
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
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