From 1aa718a4a731cf6b1a4b3b2d1aa81469718d4c69 Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Tue, 25 Apr 2023 05:00:09 +0000 Subject: [PATCH 01/10] Add force_reinstall option --- micropip/_commands/install.py | 13 +++++++++++++ micropip/_commands/uninstall.py | 14 ++++++++++---- micropip/transaction.py | 11 +++++++---- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index 53fad75..f66ea77 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -7,6 +7,7 @@ from .._compat import loadPackage, to_js from ..constants import FAQ_URLS from ..transaction import Transaction +from .uninstall import uninstall async def install( @@ -15,6 +16,7 @@ async def install( deps: bool = True, credentials: str | None = None, pre: bool = False, + force_reinstall: bool = False, ) -> None: """Install the given package and all of its dependencies. @@ -83,6 +85,9 @@ async def install( If ``True``, include pre-release and development versions. By default, micropip only finds stable versions. + force_reinstall : + + If ``True``, reinstall all packages even if they are already up-to-date. """ ctx = default_environment() if isinstance(requirements, str): @@ -106,6 +111,7 @@ async def install( keep_going=keep_going, deps=deps, pre=pre, + force_reinstall=force_reinstall, fetch_kwargs=fetch_kwargs, ) await transaction.gather_requirements(requirements) @@ -117,6 +123,13 @@ async def install( f"See: {FAQ_URLS['cant_find_wheel']}\n" ) + # uninstall packages that are installed + uninstall_packages = set([pkg.name for pkg in transaction.wheels]) | set( + [pkg.name for pkg in transaction.pyodide_packages] + ) + + uninstall(uninstall_packages, _ignore_missing=True) + wheel_promises = [] # Install built-in packages pyodide_packages = transaction.pyodide_packages diff --git a/micropip/_commands/uninstall.py b/micropip/_commands/uninstall.py index 5a71469..032e0a7 100644 --- a/micropip/_commands/uninstall.py +++ b/micropip/_commands/uninstall.py @@ -1,13 +1,14 @@ import importlib import importlib.metadata import warnings +from collections.abc import Iterable 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: +def uninstall(packages: str | Iterable[str], *, _ignore_missing: bool = False) -> None: """Uninstall the given packages. This function only supports uninstalling packages that are installed @@ -22,6 +23,9 @@ def uninstall(packages: str | list[str]) -> None: ---------- packages Packages to uninstall. + + _ignore_missing + If ``True``, suppress warnings when a package is not installed. """ if isinstance(packages, str): @@ -33,9 +37,11 @@ def uninstall(packages: str | list[str]) -> None: dist = importlib.metadata.distribution(package) distributions.append(dist) except importlib.metadata.PackageNotFoundError: - warnings.warn( - f"WARNING: Skipping '{package}' as it is not installed.", stacklevel=1 - ) + if not _ignore_missing: # TODO: Can we utilize log level here? + warnings.warn( + f"WARNING: Skipping '{package}' as it is not installed.", + stacklevel=1, + ) for dist in distributions: # Note: this value needs to be retrieved before removing files, as diff --git a/micropip/transaction.py b/micropip/transaction.py index 4fe6830..291cfd1 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -230,6 +230,7 @@ class Transaction: keep_going: bool deps: bool pre: bool + force_reinstall: bool fetch_kwargs: dict[str, str] locked: dict[str, PackageMetadata] = field(default_factory=dict) @@ -261,6 +262,10 @@ async def add_requirement(self, req: str | Requirement) -> None: await self.add_wheel(wheel, extras=set()) def check_version_satisfied(self, req: Requirement) -> bool: + """ + Check if the installed version of a package satisfies the requirement. + Returns True if the requirement is satisfied, False otherwise. + """ ver = None try: ver = importlib.metadata.version(req.name) @@ -276,9 +281,7 @@ def check_version_satisfied(self, req: Requirement) -> bool: # installed version matches, nothing to do return True - raise ValueError( - f"Requested '{req}', " f"but {req.name}=={ver} is already installed" - ) + return False async def add_requirement_inner( self, @@ -330,7 +333,7 @@ def eval_marker(e: dict[str, str]) -> bool: return # Is some version of this package is already installed? req.name = canonicalize_name(req.name) - if self.check_version_satisfied(req): + if not self.force_reinstall and self.check_version_satisfied(req): return # If there's a Pyodide package that matches the version constraint, use From 85e24155a9166bf0db24a8cc118c97af534e79f3 Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Tue, 25 Apr 2023 05:27:22 +0000 Subject: [PATCH 02/10] Add tests --- micropip/_commands/install.py | 4 +-- micropip/_commands/uninstall.py | 4 +-- micropip/transaction.py | 2 +- tests/conftest.py | 10 ++++++ tests/test_install.py | 59 +++++++++++++++++++++++++++++++++ tests/test_transaction.py | 1 + 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index f66ea77..ed405f8 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -124,11 +124,11 @@ async def install( ) # uninstall packages that are installed - uninstall_packages = set([pkg.name for pkg in transaction.wheels]) | set( + packages_all = set([pkg.name for pkg in transaction.wheels]) | set( [pkg.name for pkg in transaction.pyodide_packages] ) - uninstall(uninstall_packages, _ignore_missing=True) + uninstall(packages_all, ignore_missing=True) wheel_promises = [] # Install built-in packages diff --git a/micropip/_commands/uninstall.py b/micropip/_commands/uninstall.py index 032e0a7..46f52ae 100644 --- a/micropip/_commands/uninstall.py +++ b/micropip/_commands/uninstall.py @@ -8,7 +8,7 @@ from .._utils import get_files_in_distribution, get_root -def uninstall(packages: str | Iterable[str], *, _ignore_missing: bool = False) -> None: +def uninstall(packages: str | Iterable[str], *, ignore_missing: bool = False) -> None: """Uninstall the given packages. This function only supports uninstalling packages that are installed @@ -37,7 +37,7 @@ def uninstall(packages: str | Iterable[str], *, _ignore_missing: bool = False) - dist = importlib.metadata.distribution(package) distributions.append(dist) except importlib.metadata.PackageNotFoundError: - if not _ignore_missing: # TODO: Can we utilize log level here? + if not ignore_missing: # TODO: Can we utilize log level here? warnings.warn( f"WARNING: Skipping '{package}' as it is not installed.", stacklevel=1, diff --git a/micropip/transaction.py b/micropip/transaction.py index 291cfd1..c4c218d 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -358,7 +358,7 @@ def eval_marker(e: dict[str, str]) -> bool: else: return - if self.check_version_satisfied(req): + if not self.force_reinstall and self.check_version_satisfied(req): # Maybe while we were downloading pypi_json some other branch # installed the wheel? return diff --git a/tests/conftest.py b/tests/conftest.py index 64e661c..f427067 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,10 +108,20 @@ def _mock_importlib_version(name: str) -> str: def _mock_importlib_distributions(): return (Distribution.at(p) for p in wheel_base.glob("*.dist-info")) # type: ignore[union-attr] + def _mock_importlib_distribution(name: str) -> Distribution: + for dist in _mock_importlib_distributions(): + if dist.name == name: + return dist + + raise PackageNotFoundError(name) + monkeypatch.setattr(importlib.metadata, "version", _mock_importlib_version) monkeypatch.setattr( importlib.metadata, "distributions", _mock_importlib_distributions ) + monkeypatch.setattr( + importlib.metadata, "distribution", _mock_importlib_distribution + ) class Wildcard: diff --git a/tests/test_install.py b/tests/test_install.py index 61818ff..e15fb17 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -360,3 +360,62 @@ async def run_test(selenium, url, wheel_name): ] run_test(selenium_standalone_micropip, url, SNOWBALL_WHEEL) + + +@pytest.mark.asyncio +async def test_reinstall_different_version( + mock_fetch: mock_fetch_cls, + mock_importlib, +) -> None: + import importlib.metadata + + dummy = "dummy" + version_old = "1.0.0" + version_new = "2.0.0" + + mock_fetch.add_pkg_version(dummy, version_old) + mock_fetch.add_pkg_version(dummy, version_new) + + await micropip.install(f"{dummy}=={version_old}") + assert micropip.list()[dummy].version == version_old + assert importlib.metadata.version(dummy) == version_old + + await micropip.install(f"{dummy}=={version_new}") + assert micropip.list()[dummy].version == version_new + assert importlib.metadata.version(dummy) == version_new + + await micropip.install(f"{dummy}=={version_old}") + assert micropip.list()[dummy].version == version_old + assert importlib.metadata.version(dummy) == version_old + + +@pytest.mark.asyncio +async def test_force_reinstall( + mock_fetch: mock_fetch_cls, + mock_importlib, +) -> None: + import importlib.metadata + + dummy = "dummy" + version_old = "1.0.0" + + mock_fetch.add_pkg_version(dummy, version_old) + + await micropip.install(f"{dummy}=={version_old}") + assert micropip.list()[dummy].version == version_old + assert importlib.metadata.version(dummy) == version_old + + dist_path = importlib.metadata.distribution(dummy)._path # type: ignore[attr-defined] + assert dist_path.exists() + + # create a dummy file in the dist_info directory, then force reinstall + # the package. The dummy file should be removed. + dummy_file = dist_path / "dummy" + dummy_file.touch() + assert dummy_file.exists() + + await micropip.install(f"{dummy}=={version_old}", force_reinstall=True) + assert micropip.list()[dummy].version == version_old + assert importlib.metadata.version(dummy) == version_old + + assert not dummy_file.exists() diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 4a67238..05aa559 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -63,6 +63,7 @@ def create_transaction(Transaction): ctx={}, ctx_extras=[], fetch_kwargs={}, + force_reinstall=False, ) From 4068fe2aa3457da3079cc4eb1008c889dea29ecf Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Tue, 25 Apr 2023 09:49:29 +0000 Subject: [PATCH 03/10] Some refactorings --- micropip/_commands/install.py | 11 ++++- micropip/_commands/uninstall.py | 64 +++------------------------ micropip/_uninstall.py | 78 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 micropip/_uninstall.py diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index ed405f8..284e8eb 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -5,9 +5,9 @@ from packaging.markers import default_environment from .._compat import loadPackage, to_js +from .._uninstall import _uninstall from ..constants import FAQ_URLS from ..transaction import Transaction -from .uninstall import uninstall async def install( @@ -128,7 +128,14 @@ async def install( [pkg.name for pkg in transaction.pyodide_packages] ) - uninstall(packages_all, ignore_missing=True) + distributions = [] + for pkg_name in packages_all: + try: + distributions.append(importlib.metadata.distribution(pkg_name)) + except importlib.metadata.PackageNotFoundError: + pass + + _uninstall(distributions) wheel_promises = [] # Install built-in packages diff --git a/micropip/_commands/uninstall.py b/micropip/_commands/uninstall.py index 46f52ae..220e1ca 100644 --- a/micropip/_commands/uninstall.py +++ b/micropip/_commands/uninstall.py @@ -4,11 +4,10 @@ from collections.abc import Iterable from importlib.metadata import Distribution -from .._compat import loadedPackages -from .._utils import get_files_in_distribution, get_root +from .._uninstall import _uninstall -def uninstall(packages: str | Iterable[str], *, ignore_missing: bool = False) -> None: +def uninstall(packages: str | Iterable[str]) -> None: """Uninstall the given packages. This function only supports uninstalling packages that are installed @@ -23,9 +22,6 @@ def uninstall(packages: str | Iterable[str], *, ignore_missing: bool = False) -> ---------- packages Packages to uninstall. - - _ignore_missing - If ``True``, suppress warnings when a package is not installed. """ if isinstance(packages, str): @@ -37,61 +33,11 @@ def uninstall(packages: str | Iterable[str], *, ignore_missing: bool = False) -> dist = importlib.metadata.distribution(package) distributions.append(dist) except importlib.metadata.PackageNotFoundError: - if not ignore_missing: # TODO: Can we utilize log level here? - warnings.warn( - f"WARNING: Skipping '{package}' as it is not installed.", - stacklevel=1, - ) - - 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.", - stacklevel=1, - ) - - 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. ", - stacklevel=1, - ) - - 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.", + f"WARNING: Skipping '{package}' as it is not installed.", stacklevel=1, ) + _uninstall(distributions) + importlib.invalidate_caches() diff --git a/micropip/_uninstall.py b/micropip/_uninstall.py new file mode 100644 index 0000000..d2427af --- /dev/null +++ b/micropip/_uninstall.py @@ -0,0 +1,78 @@ +import importlib +import importlib.metadata +import warnings +from collections.abc import Iterable +from importlib.metadata import Distribution + +from ._compat import loadedPackages +from ._utils import get_files_in_distribution, get_root + + +def _uninstall(distributions: Iterable[Distribution]) -> None: + """Uninstall the given package distributions. + + This function does not do any checks, so make sure that the distributions + are installed and that they are installed using a wheel file, i.e. packages + that have distribution metadata. + + This function also does not invalidate the import cache, so make sure to + call `importlib.invalidate_caches()` after calling this function. + + Parameters + ---------- + distributions + Package distributions to uninstall. + """ + + 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.", + stacklevel=1, + ) + + 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. ", + stacklevel=1, + ) + + 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.", + stacklevel=1, + ) + + importlib.invalidate_caches() From 35fc850e5bde1ac36290c9e032a32d6ce0542334 Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Tue, 25 Apr 2023 10:09:58 +0000 Subject: [PATCH 04/10] Rename internal method --- micropip/_commands/install.py | 4 ++-- micropip/_commands/uninstall.py | 4 ++-- micropip/_uninstall.py | 6 +----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index 284e8eb..b9e251d 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -5,7 +5,7 @@ from packaging.markers import default_environment from .._compat import loadPackage, to_js -from .._uninstall import _uninstall +from .._uninstall import uninstall_distributions from ..constants import FAQ_URLS from ..transaction import Transaction @@ -135,7 +135,7 @@ async def install( except importlib.metadata.PackageNotFoundError: pass - _uninstall(distributions) + uninstall_distributions(distributions) wheel_promises = [] # Install built-in packages diff --git a/micropip/_commands/uninstall.py b/micropip/_commands/uninstall.py index 220e1ca..a8a7ca9 100644 --- a/micropip/_commands/uninstall.py +++ b/micropip/_commands/uninstall.py @@ -4,7 +4,7 @@ from collections.abc import Iterable from importlib.metadata import Distribution -from .._uninstall import _uninstall +from .._uninstall import uninstall_distributions def uninstall(packages: str | Iterable[str]) -> None: @@ -38,6 +38,6 @@ def uninstall(packages: str | Iterable[str]) -> None: stacklevel=1, ) - _uninstall(distributions) + uninstall_distributions(distributions) importlib.invalidate_caches() diff --git a/micropip/_uninstall.py b/micropip/_uninstall.py index d2427af..7431c8a 100644 --- a/micropip/_uninstall.py +++ b/micropip/_uninstall.py @@ -1,5 +1,3 @@ -import importlib -import importlib.metadata import warnings from collections.abc import Iterable from importlib.metadata import Distribution @@ -8,7 +6,7 @@ from ._utils import get_files_in_distribution, get_root -def _uninstall(distributions: Iterable[Distribution]) -> None: +def uninstall_distributions(distributions: Iterable[Distribution]) -> None: """Uninstall the given package distributions. This function does not do any checks, so make sure that the distributions @@ -74,5 +72,3 @@ def _uninstall(distributions: Iterable[Distribution]) -> None: f"WARNING: a package '{name}' was not found in loadedPackages.", stacklevel=1, ) - - importlib.invalidate_caches() From 7ad86b7a675ef9040e034f924865136047f5a22d Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Thu, 4 May 2023 06:17:01 +0000 Subject: [PATCH 05/10] Rename force_reinstall to reinstall --- micropip/_commands/install.py | 8 ++++---- micropip/transaction.py | 38 +++++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index 9811d48..1e8a6f9 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -17,7 +17,7 @@ async def install( deps: bool = True, credentials: str | None = None, pre: bool = False, - force_reinstall: bool = False, + reinstall: bool = False, *, verbose: bool | int = False, ) -> None: @@ -88,9 +88,9 @@ async def install( If ``True``, include pre-release and development versions. By default, micropip only finds stable versions. - force_reinstall : + reinstall : - If ``True``, reinstall all packages even if they are already up-to-date. + If ``True``, reinstall packages if they are already installed. verbose : Print more information about the process. @@ -121,7 +121,7 @@ async def install( keep_going=keep_going, deps=deps, pre=pre, - force_reinstall=force_reinstall, + reinstall=reinstall, fetch_kwargs=fetch_kwargs, verbose=verbose, ) diff --git a/micropip/transaction.py b/micropip/transaction.py index adfaccd..4cecaa1 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -233,7 +233,7 @@ class Transaction: keep_going: bool deps: bool pre: bool - force_reinstall: bool + reinstall: bool fetch_kwargs: dict[str, str] locked: dict[str, PackageMetadata] = field(default_factory=dict) @@ -266,10 +266,20 @@ async def add_requirement(self, req: str | Requirement) -> None: await self.add_wheel(wheel, extras=set(), specifier="") - def check_version_satisfied(self, req: Requirement) -> tuple[bool, str]: + def check_version_satisfied( + self, req: Requirement, *, allow_reinstall: bool = False + ) -> tuple[bool, str]: """ Check if the installed version of a package satisfies the requirement. Returns True if the requirement is satisfied, False otherwise. + + Parameters + ---------- + req + The requirement to check. + allow_reinstall + If False, this function will raise exception if the package is already installed + and the installed version does not satisfy the requirement. """ ver = None try: @@ -286,7 +296,16 @@ def check_version_satisfied(self, req: Requirement) -> tuple[bool, str]: # installed version matches, nothing to do return True, ver - return False, "" + if allow_reinstall: + return False, "" + else: + raise ValueError( + f"Requested '{req}', " + f"but {req.name}=={ver} is already installed. " + "If you want to reinstall the package with a different version, " + "use micropip.install(..., reinstall=True) to force reinstall, " + "or micropip.uninstall(...) to uninstall the package first." + ) async def add_requirement_inner( self, @@ -339,9 +358,10 @@ def eval_marker(e: dict[str, str]) -> bool: # Is some version of this package is already installed? req.name = canonicalize_name(req.name) - satisfied, ver = self.check_version_satisfied(req) - - if not self.force_reinstall and satisfied: + satisfied, ver = self.check_version_satisfied( + req, allow_reinstall=self.reinstall + ) + if satisfied: logger.info(f"Requirement already satisfied: {req} ({ver})") return @@ -367,8 +387,10 @@ def eval_marker(e: dict[str, str]) -> bool: else: return - satisfied, ver = self.check_version_satisfied(req) - if not self.force_reinstall and satisfied: + satisfied, ver = self.check_version_satisfied( + req, allow_reinstall=self.reinstall + ) + if satisfied: # Maybe while we were downloading pypi_json some other branch # installed the wheel? logger.info(f"Requirement already satisfied: {req} ({ver})") From 03a902bd73ac0414ba79e2633db6cf6abbd60b98 Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Thu, 4 May 2023 06:18:55 +0000 Subject: [PATCH 06/10] Update test --- tests/test_install.py | 37 +++++-------------------------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/tests/test_install.py b/tests/test_install.py index 2770af7..a46378f 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -370,6 +370,8 @@ async def test_reinstall_different_version( ) -> None: import importlib.metadata + import pytest + dummy = "dummy" version_old = "1.0.0" version_new = "2.0.0" @@ -385,42 +387,13 @@ async def test_reinstall_different_version( assert micropip.list()[dummy].version == version_new assert importlib.metadata.version(dummy) == version_new - await micropip.install(f"{dummy}=={version_old}") - assert micropip.list()[dummy].version == version_old - assert importlib.metadata.version(dummy) == version_old - - -@pytest.mark.asyncio -async def test_force_reinstall( - mock_fetch: mock_fetch_cls, - mock_importlib, -) -> None: - import importlib.metadata + with pytest.raises(ValueError, match="already installed"): + await micropip.install(f"{dummy}=={version_old}", reinstall=False) - dummy = "dummy" - version_old = "1.0.0" - - mock_fetch.add_pkg_version(dummy, version_old) - - await micropip.install(f"{dummy}=={version_old}") + await micropip.install(f"{dummy}=={version_old}", reinstall=False) assert micropip.list()[dummy].version == version_old assert importlib.metadata.version(dummy) == version_old - dist_path = importlib.metadata.distribution(dummy)._path # type: ignore[attr-defined] - assert dist_path.exists() - - # create a dummy file in the dist_info directory, then force reinstall - # the package. The dummy file should be removed. - dummy_file = dist_path / "dummy" - dummy_file.touch() - assert dummy_file.exists() - - await micropip.install(f"{dummy}=={version_old}", force_reinstall=True) - assert micropip.list()[dummy].version == version_old - assert importlib.metadata.version(dummy) == version_old - - assert not dummy_file.exists() - def test_logging(selenium_standalone_micropip): # TODO: make a fixture for this, it's used in a few places From bdac07fe0b032035130fb75e1b375304955e8ceb Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Thu, 4 May 2023 06:20:07 +0000 Subject: [PATCH 07/10] Indent log --- micropip/_commands/install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/micropip/_commands/install.py b/micropip/_commands/install.py index 1e8a6f9..3a8bf1b 100644 --- a/micropip/_commands/install.py +++ b/micropip/_commands/install.py @@ -7,7 +7,7 @@ from .._compat import loadPackage, to_js from .._uninstall import uninstall_distributions from ..constants import FAQ_URLS -from ..logging import setup_logging +from ..logging import indent_log, setup_logging from ..transaction import Transaction @@ -146,7 +146,8 @@ async def install( except importlib.metadata.PackageNotFoundError: pass - uninstall_distributions(distributions) + with indent_log(): + uninstall_distributions(distributions) if packages_all: logger.info("Installing collected packages: " + ", ".join(packages_all)) From a0196c38ca687272603bdb09e0af9713c9652c4e Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Thu, 4 May 2023 06:25:26 +0000 Subject: [PATCH 08/10] Fix tests --- tests/test_install.py | 2 +- tests/test_transaction.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_install.py b/tests/test_install.py index a46378f..e7f5b67 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -390,7 +390,7 @@ async def test_reinstall_different_version( with pytest.raises(ValueError, match="already installed"): await micropip.install(f"{dummy}=={version_old}", reinstall=False) - await micropip.install(f"{dummy}=={version_old}", reinstall=False) + await micropip.install(f"{dummy}=={version_old}", reinstall=True) assert micropip.list()[dummy].version == version_old assert importlib.metadata.version(dummy) == version_old diff --git a/tests/test_transaction.py b/tests/test_transaction.py index a40709a..65d368c 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -63,7 +63,7 @@ def create_transaction(Transaction): ctx={}, ctx_extras=[], fetch_kwargs={}, - force_reinstall=False, + reinstall=False, ) From f688b0ec5df157d693fcfa211020022889e5572f Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Mon, 8 May 2023 07:22:46 +0000 Subject: [PATCH 09/10] Fix test --- tests/test_install.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_install.py b/tests/test_install.py index e7f5b67..934faa1 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -379,10 +379,6 @@ async def test_reinstall_different_version( mock_fetch.add_pkg_version(dummy, version_old) mock_fetch.add_pkg_version(dummy, version_new) - await micropip.install(f"{dummy}=={version_old}") - assert micropip.list()[dummy].version == version_old - assert importlib.metadata.version(dummy) == version_old - await micropip.install(f"{dummy}=={version_new}") assert micropip.list()[dummy].version == version_new assert importlib.metadata.version(dummy) == version_new From 211559b09f9a2d29d1a7887ef4887eb2ec109e52 Mon Sep 17 00:00:00 2001 From: ryanking13 Date: Mon, 8 May 2023 07:24:16 +0000 Subject: [PATCH 10/10] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe35a4..465de4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `verbose` parameter to micropip.install and micropip.uninstall [#60](https://github.com/pyodide/micropip/pull/60) +- Added `reinstall` parameter to micropip.install to allow reinstalling + a package that is already installed + [#64](https://github.com/pyodide/micropip/pull/64) + ### Fixed - Fixed `micropip.add_mock_package` to work with Pyodide>=0.23.0