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}")