From 9a686d148e5d98e559fccccb9ba6fcd28adaf1e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Mar 2021 21:12:02 -1000 Subject: [PATCH] Ensure startup can proceed when there is package metadata cruft (#47706) If a package fails to install or partially installed importlib version can return None. We now try pkg_resources first, then try importlib, and handle the case where version unexpectedly returns None --- homeassistant/util/package.py | 12 ++++++++- tests/util/test_package.py | 47 ++++++++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 5391d92ed89..34628b4ca4d 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -34,6 +34,9 @@ def is_installed(package: str) -> bool: Returns False when the package is not installed or doesn't meet req. """ try: + pkg_resources.get_distribution(package) + return True + except (pkg_resources.ResolutionError, pkg_resources.ExtractionError): req = pkg_resources.Requirement.parse(package) except ValueError: # This is a zip file. We no longer use this in Home Assistant, @@ -41,7 +44,14 @@ def is_installed(package: str) -> bool: req = pkg_resources.Requirement.parse(urlparse(package).fragment) try: - return version(req.project_name) in req + installed_version = version(req.project_name) + # This will happen when an install failed or + # was aborted while in progress see + # https://github.com/home-assistant/core/issues/47699 + if installed_version is None: + _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore + return False + return installed_version in req except PackageNotFoundError: return False diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 0c251662444..494fe5fa11f 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -239,10 +239,55 @@ async def test_async_get_user_site(mock_env_copy): def test_check_package_global(): """Test for an installed package.""" - installed_package = list(pkg_resources.working_set)[0].project_name + first_package = list(pkg_resources.working_set)[0] + installed_package = first_package.project_name + installed_version = first_package.version + assert package.is_installed(installed_package) + assert package.is_installed(f"{installed_package}=={installed_version}") + assert package.is_installed(f"{installed_package}>={installed_version}") + assert package.is_installed(f"{installed_package}<={installed_version}") + assert not package.is_installed(f"{installed_package}<{installed_version}") + + +def test_check_package_version_does_not_match(): + """Test for version mismatch.""" + installed_package = list(pkg_resources.working_set)[0].project_name + assert not package.is_installed(f"{installed_package}==999.999.999") + assert not package.is_installed(f"{installed_package}>=999.999.999") def test_check_package_zip(): """Test for an installed zip package.""" assert not package.is_installed(TEST_ZIP_REQ) + + +def test_get_distribution_falls_back_to_version(): + """Test for get_distribution failing and fallback to version.""" + first_package = list(pkg_resources.working_set)[0] + installed_package = first_package.project_name + installed_version = first_package.version + + with patch( + "homeassistant.util.package.pkg_resources.get_distribution", + side_effect=pkg_resources.ExtractionError, + ): + assert package.is_installed(installed_package) + assert package.is_installed(f"{installed_package}=={installed_version}") + assert package.is_installed(f"{installed_package}>={installed_version}") + assert package.is_installed(f"{installed_package}<={installed_version}") + assert not package.is_installed(f"{installed_package}<{installed_version}") + + +def test_check_package_previous_failed_install(): + """Test for when a previously install package failed and left cruft behind.""" + first_package = list(pkg_resources.working_set)[0] + installed_package = first_package.project_name + installed_version = first_package.version + + with patch( + "homeassistant.util.package.pkg_resources.get_distribution", + side_effect=pkg_resources.ExtractionError, + ), patch("homeassistant.util.package.version", return_value=None): + assert not package.is_installed(installed_package) + assert not package.is_installed(f"{installed_package}=={installed_version}")