From 86f40d68c14ddea332a761d485aa029f60134032 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Fri, 18 Oct 2024 15:47:49 +0200 Subject: [PATCH 1/2] Differenctiate `ValueError`s ValueErrors are a bit too generic, and by raising/catching them we loose a potential useful information; For example an invalid content type for the index, is dealt as the same way than a missing wheel. This introduces new exceptions for various cases, ant stop catching the now UnsupportedContentTypeError, as I believe it should be a hard error as we are likely having either an error with the index software, or a user configuraition error. See also beginning of discussion in #142 --- micropip/package_index.py | 21 ++++++++++++++------- micropip/transaction.py | 29 ++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/micropip/package_index.py b/micropip/package_index.py index f7cf02a..0e65f51 100644 --- a/micropip/package_index.py +++ b/micropip/package_index.py @@ -218,6 +218,10 @@ def _contain_placeholder(url: str, placeholder: str = "package_name") -> bool: return placeholder in fields +class UnsupportedContentTypeError(Exception): + pass + + def _select_parser(content_type: str, pkgname: str) -> Callable[[str], ProjectInfo]: """ Select the function to parse the response based on the content type. @@ -234,7 +238,13 @@ def _select_parser(content_type: str, pkgname: str) -> Callable[[str], ProjectIn ): return partial(ProjectInfo.from_simple_html_api, pkgname=pkgname) case _: - raise ValueError(f"Unsupported content type: {content_type}") + raise UnsupportedContentTypeError( + f"Unsupported content type: {content_type}" + ) + + +class NoValidIndexForPackageError(Exception): + pass async def query_package( @@ -296,14 +306,11 @@ async def query_package( raise content_type = headers.get("content-type", "").lower() - try: - parser = _select_parser(content_type, name) - except ValueError as e: - raise ValueError(f"Error trying to decode url: {url}") from e + parser = _select_parser(content_type, name) return parser(metadata) else: - raise ValueError( - f"Can't fetch metadata for '{name}'. " + raise NoValidIndexForPackageError( + f"Can't fetch metadata for {name!r}. " "Please make sure you have entered a correct package name " "and correctly specified index_urls (if you changed them)." ) diff --git a/micropip/transaction.py b/micropip/transaction.py index 9d64a72..b901e43 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -14,7 +14,7 @@ from ._utils import best_compatible_tag_index, check_compatible from .constants import FAQ_URLS from .package import PackageMetadata -from .package_index import ProjectInfo +from .package_index import NoValidIndexForPackageError, ProjectInfo from .wheelinfo import WheelInfo logger = logging.getLogger("micropip") @@ -153,12 +153,23 @@ def eval_marker(e: dict[str, str]) -> bool: else: try: await self._add_requirement_from_package_index(req) - except ValueError: - logger.debug( - "Transaction: package %r not found in index, will search lock file", + except NoValidIndexForPackageError: + logger.warning( + "Transaction: package %r was not found in any index, " + "falling back to pyodide lock file", + req, + ) + # This is most likely an error, and it's likely we should + # hard fail here, but the legacy behavior was to ignore + # errors. + if not self._add_requirement_from_pyodide_lock(req): + raise + except NoCompatibleWheelError: + logger.warning( + "Transaction: no compatible wheels for package %r was " + "found falling back to pyodide lock file", req, ) - # If the requirement is not found in package index, # we still have a chance to find it from pyodide lockfile. if not self._add_requirement_from_pyodide_lock(req): @@ -167,7 +178,7 @@ def eval_marker(e: dict[str, str]) -> bool: ) raise - except ValueError: + except (NoCompatibleWheelError, NoValidIndexForPackageError): self.failed.append(req) if not self.keep_going: raise @@ -252,6 +263,10 @@ async def add_wheel( self.wheels.append(wheel) +class NoCompatibleWheelError(Exception): + pass + + def find_wheel(metadata: ProjectInfo, req: Requirement) -> WheelInfo: """Parse metadata to find the latest version of pure python wheel. Parameters @@ -292,7 +307,7 @@ def find_wheel(metadata: ProjectInfo, req: Requirement) -> WheelInfo: if best_wheel is not None: return wheel - raise ValueError( + raise NoCompatibleWheelError( f"Can't find a pure Python 3 wheel for '{req}'.\n" f"See: {FAQ_URLS['cant_find_wheel']}\n" "You can use `await micropip.install(..., keep_going=True)` " From cfb74836dc7faa513ffa680a7f953cadc0e8c9b6 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Mon, 21 Oct 2024 15:28:09 +0200 Subject: [PATCH 2/2] Move errors to a separate file. --- micropip/errors.py | 24 ++++++++++++++++++++++++ micropip/package_index.py | 11 ++--------- micropip/transaction.py | 11 ++++------- 3 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 micropip/errors.py diff --git a/micropip/errors.py b/micropip/errors.py new file mode 100644 index 0000000..0ace81f --- /dev/null +++ b/micropip/errors.py @@ -0,0 +1,24 @@ +""" +A module to define micropip custom Exceptions. +""" + + +class UnsupportedContentTypeError(Exception): + """raise when selecting a parser for current index + + This is raise if the current content type is not recognized. + """ + + +class NoCompatibleWheelError(Exception): + """ + This is raised when a package is found but have no wheel compatible with + current pyodide. + """ + + +class PackageNotFoundOnAnyIndexError(Exception): + """ + This is raised if current package was not found on any of the currently + listed index. + """ diff --git a/micropip/package_index.py b/micropip/package_index.py index 0e65f51..afd36b1 100644 --- a/micropip/package_index.py +++ b/micropip/package_index.py @@ -13,6 +13,7 @@ from ._compat import HttpStatusError, fetch_string_and_headers from ._utils import is_package_compatible, parse_version +from .errors import PackageNotFoundOnAnyIndexError, UnsupportedContentTypeError from .externals.mousebender.simple import from_project_details_html from .wheelinfo import WheelInfo @@ -218,10 +219,6 @@ def _contain_placeholder(url: str, placeholder: str = "package_name") -> bool: return placeholder in fields -class UnsupportedContentTypeError(Exception): - pass - - def _select_parser(content_type: str, pkgname: str) -> Callable[[str], ProjectInfo]: """ Select the function to parse the response based on the content type. @@ -243,10 +240,6 @@ def _select_parser(content_type: str, pkgname: str) -> Callable[[str], ProjectIn ) -class NoValidIndexForPackageError(Exception): - pass - - async def query_package( name: str, fetch_kwargs: dict[str, Any] | None = None, @@ -309,7 +302,7 @@ async def query_package( parser = _select_parser(content_type, name) return parser(metadata) else: - raise NoValidIndexForPackageError( + raise PackageNotFoundOnAnyIndexError( f"Can't fetch metadata for {name!r}. " "Please make sure you have entered a correct package name " "and correctly specified index_urls (if you changed them)." diff --git a/micropip/transaction.py b/micropip/transaction.py index b901e43..5223af3 100644 --- a/micropip/transaction.py +++ b/micropip/transaction.py @@ -13,8 +13,9 @@ from ._compat import REPODATA_PACKAGES from ._utils import best_compatible_tag_index, check_compatible from .constants import FAQ_URLS +from .errors import NoCompatibleWheelError from .package import PackageMetadata -from .package_index import NoValidIndexForPackageError, ProjectInfo +from .package_index import PackageNotFoundOnAnyIndexError, ProjectInfo from .wheelinfo import WheelInfo logger = logging.getLogger("micropip") @@ -153,7 +154,7 @@ def eval_marker(e: dict[str, str]) -> bool: else: try: await self._add_requirement_from_package_index(req) - except NoValidIndexForPackageError: + except PackageNotFoundOnAnyIndexError: logger.warning( "Transaction: package %r was not found in any index, " "falling back to pyodide lock file", @@ -178,7 +179,7 @@ def eval_marker(e: dict[str, str]) -> bool: ) raise - except (NoCompatibleWheelError, NoValidIndexForPackageError): + except (NoCompatibleWheelError, PackageNotFoundOnAnyIndexError): self.failed.append(req) if not self.keep_going: raise @@ -263,10 +264,6 @@ async def add_wheel( self.wheels.append(wheel) -class NoCompatibleWheelError(Exception): - pass - - def find_wheel(metadata: ProjectInfo, req: Requirement) -> WheelInfo: """Parse metadata to find the latest version of pure python wheel. Parameters