-
-
Notifications
You must be signed in to change notification settings - Fork 43
ENH Add micropip.uninstall() #55
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
Merged
Merged
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
3a3a53f
Update flake8 rules
ryanking13 fd6f3fc
Explicitly set package directory
ryanking13 1d315cb
Move fixtures to conftest
ryanking13 e24bf2e
Implement uninstall
ryanking13 05ea7a7
Do not load pyparsing
ryanking13 48f6975
Write basic test
ryanking13 a92b729
Add more tests
ryanking13 3185f03
Add importlib helpers
ryanking13 f410fd0
Merge remote-tracking branch 'upstream/main' into uninstall
ryanking13 0f77c3c
Remove extra config
ryanking13 6db6c3f
Simplify removal
ryanking13 34f7671
Ignore only FileNotFoundError when removal fails
ryanking13 8138e3f
Invalidate cache after removal
ryanking13 cce5223
Update tests
ryanking13 0d28c8a
Merge remote-tracking branch 'upstream/main' into uninstall
ryanking13 ea7832b
Remove loadedPackages attribute after uninstall
ryanking13 595324a
Update tests
ryanking13 e4f42d5
Apply linter
ryanking13 5ad8718
Show warning instead of error if a package is not installed
ryanking13 c85cddb
Fix how files are retrieved
ryanking13 165181e
Fix missing continue statement
ryanking13 dc6d3f8
Add test wheel
ryanking13 d1307ad
Use test wheel and add more tests
ryanking13 6801c13
Add changelog
ryanking13 555d609
Split common methods into a separate file
ryanking13 f5e4d7d
Fix typo
ryanking13 ac43603
Update docstring
ryanking13 c6e3ca0
Make utility functions private
ryanking13 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import importlib | ||
| import importlib.metadata | ||
| import warnings | ||
| from importlib.metadata import Distribution | ||
|
|
||
| from ._compat import loadedPackages | ||
| from .utils import get_files_in_distribution, get_root | ||
|
|
||
|
|
||
| def uninstall(packages: str | list[str]) -> None: | ||
| """Uninstall the given packages. | ||
|
|
||
| This function only supports uninstalling packages that are installed | ||
| using a wheel file, i.e. packages that have distribution metadata. | ||
|
|
||
| It is possible to reinstall a package after uninstalling it, but | ||
| note that modules / functions that are already imported will not be | ||
| automatically removed from the namespace. So make sure to reload | ||
| the module after reinstalling by e.g. running `importlib.reload(module)`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| packages | ||
| Packages to uninstall. | ||
| """ | ||
|
|
||
| if isinstance(packages, str): | ||
| packages = [packages] | ||
|
|
||
| distributions: list[Distribution] = [] | ||
| for package in packages: | ||
| try: | ||
| dist = importlib.metadata.distribution(package) | ||
| distributions.append(dist) | ||
| except importlib.metadata.PackageNotFoundError: | ||
| warnings.warn(f"WARNING: Skipping '{package}' as it is not installed.") | ||
|
|
||
| for dist in distributions: | ||
| # Note: this value needs to be retrieved before removing files, as | ||
| # dist.name uses metadata file to get the name | ||
| name = dist.name | ||
|
|
||
| root = get_root(dist) | ||
| files = get_files_in_distribution(dist) | ||
| directories = set() | ||
|
|
||
| for file in files: | ||
| if not file.is_file(): | ||
| if not file.is_relative_to(root): | ||
| # This file is not in the site-packages directory. Probably one of: | ||
| # - data_files | ||
| # - scripts | ||
| # - entry_points | ||
| # Since we don't support these, we can ignore them (except for data_files (TODO)) | ||
| continue | ||
|
|
||
| warnings.warn( | ||
| f"WARNING: A file '{file}' listed in the metadata of '{dist.name}' does not exist." | ||
| ) | ||
|
|
||
| continue | ||
|
|
||
| file.unlink() | ||
|
|
||
| if file.parent != root: | ||
| directories.add(file.parent) | ||
|
|
||
| # Remove directories in reverse hierarchical order | ||
| for directory in sorted(directories, key=lambda x: len(x.parts), reverse=True): | ||
| try: | ||
| directory.rmdir() | ||
| except OSError: | ||
| warnings.warn( | ||
| f"WARNING: A directory '{directory}' is not empty after uninstallation of '{name}'. " | ||
| "This might cause problems when installing a new version of the package. " | ||
| ) | ||
|
|
||
| if hasattr(loadedPackages, name): | ||
| delattr(loadedPackages, name) | ||
| else: | ||
| # This should not happen, but just in case | ||
| warnings.warn( | ||
| f"WARNING: a package '{name}' was not found in loadedPackages." | ||
| ) | ||
|
|
||
| importlib.invalidate_caches() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| from importlib.metadata import Distribution | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def get_dist_info(dist: Distribution) -> Path: | ||
|
ryanking13 marked this conversation as resolved.
|
||
| """ | ||
| Get the .dist-info directory of a distribution. | ||
| """ | ||
| return dist._path # type: ignore[attr-defined] | ||
|
|
||
|
|
||
| def get_root(dist: Distribution) -> Path: | ||
| """ | ||
| Get the root directory where a package is installed. | ||
| This is normally the site-packages directory. | ||
| """ | ||
| return get_dist_info(dist).parent | ||
|
|
||
|
|
||
| def get_files_in_distribution(dist: Distribution) -> set[Path]: | ||
| """ | ||
| Get a list of files in a distribution, using the metadata. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| dist | ||
| Distribution to get files from. | ||
|
|
||
| Returns | ||
| ------- | ||
| A list of files in the distribution. | ||
| """ | ||
|
|
||
| root = get_root(dist) | ||
| dist_info = get_dist_info(dist) | ||
|
|
||
| files_to_remove = set() | ||
| pkg_files = dist.files or [] | ||
| metadata_files = dist_info.glob("*") | ||
|
|
||
| for file in pkg_files: | ||
| abspath = (root / file).resolve() | ||
| files_to_remove.add(abspath) | ||
|
|
||
| # Also add all files in the .dist-info directory. | ||
| # Since micropip adds some extra files there, we need to remove them too. | ||
| files_to_remove.update(metadata_files) | ||
|
|
||
| return files_to_remove | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
| from pytest_pyodide import spawn_web_server | ||
|
|
||
|
|
||
| def _build(build_dir, dist_dir): | ||
| import build | ||
| from build.env import IsolatedEnvBuilder | ||
|
|
||
| with IsolatedEnvBuilder() as env: | ||
| builder = build.ProjectBuilder(build_dir) | ||
| builder.python_executable = env.executable | ||
| builder.scripts_dir = env.scripts_dir | ||
| env.install(builder.build_system_requires) | ||
| builder.build("wheel", output_directory=dist_dir) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def wheel_path(tmp_path_factory): | ||
| # Build a micropip wheel for testing | ||
| output_dir = tmp_path_factory.mktemp("wheel") | ||
|
|
||
| _build(Path(__file__).parent.parent, output_dir) | ||
|
|
||
| yield output_dir | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def test_wheel_path(tmp_path_factory): | ||
| # Build a test wheel for testing | ||
| output_dir = tmp_path_factory.mktemp("wheel") | ||
|
|
||
| _build(Path(__file__).parent / "test_data" / "test_wheel_uninstall", output_dir) | ||
|
|
||
| yield output_dir | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def selenium_standalone_micropip(selenium_standalone, wheel_path): | ||
| """Import micropip before entering test so that global initialization of | ||
| micropip doesn't count towards hiwire refcount. | ||
| """ | ||
|
|
||
| wheel_dir = Path(wheel_path) | ||
| wheel_files = list(wheel_dir.glob("*.whl")) | ||
|
|
||
| if not wheel_files: | ||
| pytest.exit("No wheel files found in wheel/ directory") | ||
|
|
||
| wheel_file = wheel_files[0] | ||
| with spawn_web_server(wheel_dir) as server: | ||
| server_hostname, server_port, _ = server | ||
| base_url = f"http://{server_hostname}:{server_port}/" | ||
| selenium_standalone.run_js( | ||
| f""" | ||
| await pyodide.loadPackage("{base_url + wheel_file.name}"); | ||
| await pyodide.loadPackage(["packaging"]); | ||
| pyodide.runPython("import micropip"); | ||
| """ | ||
| ) | ||
|
|
||
| yield selenium_standalone |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| nonpythonfile |
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| [project] | ||
| name = "test_wheel_uninstall" | ||
| description = "Test wheel uninstall" | ||
| requires-python = ">=3.10" | ||
| version = "1.0.0" | ||
|
|
||
| [tool.setuptools] | ||
| packages = ["deep", "deep.deep", "shallow", "test_wheel_uninstall"] | ||
| py-modules = ["top_level"] | ||
|
|
||
| [tool.setuptools.package-data] | ||
| deep = ["data/*.txt"] | ||
|
|
||
| [build-system] | ||
| requires = ["setuptools>=42", "wheel"] | ||
|
|
||
| build-backend = "setuptools.build_meta" |
Empty file.
Empty file.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.