Update shortcut removal to rely on menuinst.toml#1
Conversation
| """Get recorded shortcut paths for a source from menuinst.toml.""" | ||
| data = read_menuinst_toml(prefix) | ||
| shortcuts = data.get("shortcuts", []) | ||
| return [s["path"] for s in shortcuts if s.get("source") == source and "path" in s] |
There was a problem hiding this comment.
Should they be sources be converted into Path objects to not depend on string shenanigans? Or maybe even use Path.same_file()?
There was a problem hiding this comment.
I think the idea is good but here we are unfortunately comparing filename strings (basename) and not the actual path on disk so unfortunately Path.samefile is not applicable.
There was a problem hiding this comment.
Maybe not samefile, but converting the strings into Path objects for comparison may be more robust.
There was a problem hiding this comment.
Yeah, so source here is just a filename (a basename), there is no path involved so normalization of slashes is not necessary, so we would have the same:
# Current
"foo.json" == "foo.json"
# With Path
Path("foo.json") == Path("foo.json")
I wasn't able to find any indication that source is an actual path.
|
Before I review this in detail, I have a more general question: you use the API-level removal function to remove the shortcuts if loaded from TOML. Have you considered to instead just load the paths from the TOML file? Maybe as an optional input parameter for That way, we don't have to touch any of the existing removal methods and keep everything located inside the classes. |
I did consider that approach (or something very similar) but the reason why I decided not to go with that is because:
|
| """Get recorded shortcut paths for a source from menuinst.toml.""" | ||
| data = read_menuinst_toml(prefix) | ||
| shortcuts = data.get("shortcuts", []) | ||
| return [s["path"] for s in shortcuts if s.get("source") == source and "path" in s] |
There was a problem hiding this comment.
| return [s["path"] for s in shortcuts if s.get("source") == source and "path" in s] | |
| return [Path(s["path"]) for s in shortcuts if s.get("source") == source and "path" in s] |
Since the delete_paths function returns list[Path], we might as well convert to Path early.
There was a problem hiding this comment.
This also requires us to update delete_paths() which expects list[str], I'm fine with changing both. Let me know what you think.
There was a problem hiding this comment.
Yes, changing both is good. I think we should use Path objects as much as possible.
|
|
||
| # Record shortcuts to menuinst.toml | ||
| # Only record MenuItem paths, not Menu paths (.directory files, etc.) | ||
| # The Menu's remove() method handles its own cleanup logic |
There was a problem hiding this comment.
Menu can be just as affected by drifts as MenuItem. I think Menu should be included in the record. That would favor a solution where a menus and menu items are built from the TOML file.
There was a problem hiding this comment.
The function Menu.remove() has conditional logic on all platforms, as an example:
- Linux checks if shortcuts remain,
- Windows checks if folder is empty,
- macOS is a no-op.
Recording Menu paths in the TOML file wouldn't capture this, which is why the state of the file system must be checked at removal time.
There was a problem hiding this comment.
Correct. Imagine you have a path drift where the computed value does not correspond to the original value anymore. On Windows, e.g., you'd remove the Start Menu entries, but you'd leave the folder behind since Menu still relies on the computed value.
There was a problem hiding this comment.
Hmm. I think for this PR the core issue is resolved, and I do agree that orphaned menu folders can still remain. I'm concerned this will also increase the complexity and scope of this PR but let me know what you think and I can look into it.
There was a problem hiding this comment.
My concern is that if we decide on this solution here, but then find out that it doesn't work for Menu, that we will have to refactor in the end. Let me think about this some more.
|
|
||
| def test_remove_uses_recorded_paths(self, tmp_path, monkeypatch): | ||
| """Test that remove() deletes files at recorded TOML paths, not computed paths.""" | ||
| monkeypatch.delenv("MENUINST_DISTRIBUTION_NAME", raising=False) |
There was a problem hiding this comment.
Since this placeholder is not used in the metadata of this test, you don't need to delete that variable.
| ) | ||
|
|
||
| # Verifying no exception is raised | ||
| remove(str(json_file), target_prefix=str(tmp_path), base_prefix=str(tmp_path)) |
There was a problem hiding this comment.
There should be an assert statement here that checks whether the file really was removed.
There was a problem hiding this comment.
The test currently only verifies that remove() doesn't crash when there are no TOML entries, it doesn't actually test file removal because install() was never called (no shortcut exists to remove). In order to add what you proposed I need update the test to call install() first, let me know if you still think the assert is necessary.
There was a problem hiding this comment.
I'm drawing a blank of where that exception would be raised at the moment, can you job my memory?
There was a problem hiding this comment.
There is no explicit exception, but the idea was to test that remove "works" in the sense that no unexpected error is raised.
There was a problem hiding this comment.
I think an actual remove operation would be a more convincing test.
Description
Use recorded paths from
menuinst.tomlfor removal instead of recomputing from JSON. Falls back to computed paths for legacy installs.api.pyget_recorded_paths(),delete_paths()remove()now checks TOML first, falls back to computed paths. I wasn't sure if it's totally safe to remove the "old code", I was thinking that there might be a scenario where packages are installed, then menuinst is updated to a version withmenuinst.toml, but the shortcuts installed before the update are not tracked in the.tomlfile.install()records MenuItem paths (not Menu paths) to TOMLPlatform files (
win.py,osx.py,linux.py)remove()intodelete_paths()+cleanup_side_effects(). When using TOML paths,api.remove()handles file deletion directly (it has the TOML data). But platform-specific cleanup (registry, LaunchServices, MIME etc.) still needs the MenuItem objects. Separating these two letsapi.pydelete files from TOML data while platforms handle their own side-effects.remove()preserved (calls both)Removal Flow
To simplify the review, this is the removal flow:
Checklist - did you ...
newsdirectory (using the template) for the next release's release notes?