Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-unearth for openSUSE:Factory 
checked in at 2023-06-16 16:55:01
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-unearth (Old)
 and      /work/SRC/openSUSE:Factory/.python-unearth.new.15902 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-unearth"

Fri Jun 16 16:55:01 2023 rev:4 rq:1093373 version:0.9.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-unearth/python-unearth.changes    
2022-12-16 17:52:09.324184002 +0100
+++ /work/SRC/openSUSE:Factory/.python-unearth.new.15902/python-unearth.changes 
2023-06-16 16:56:11.890186688 +0200
@@ -1,0 +2,18 @@
+Thu Jun 15 12:46:34 UTC 2023 - Andreas Schneider <a...@cryptomilk.org>
+
+- Update to version 0.9.1
+  * cli: Exposing requires-python, platform and abi interface for cli tools.  -
+    by @frostming in #52 (b9935)
+  * Evaluation issue when the requirement has no version specifier Close #50  -
+    by @frostming in #50 (0a813)
+  * Typo on json response field  -  by @frostming (3e767)
+  * Update pdm.lock  -  by @github-actions[bot] and frostming in #44 (e4572)
+  * Update pdm.lock  -  by @github-actions[bot] in #45 (a98b9)
+  * Update pdm.lock  -  by @github-actions[bot] in #47 (e0ea7)
+  * Update pdm.lock  -  by @github-actions[bot] and frostming in #49 (fc994)
+  * Allow to order the index urls and find links together for PackageFinder  -
+    by @frostming in #43 (dbe85)
+  * Allow to set prefer_binary for individual packages.
+- Use sle15_python_module_pythons
+
+-------------------------------------------------------------------

Old:
----
  unearth-0.7.0.tar.gz

New:
----
  unearth-0.9.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-unearth.spec ++++++
--- /var/tmp/diff_new_pack.6t6G93/_old  2023-06-16 16:56:12.458190039 +0200
+++ /var/tmp/diff_new_pack.6t6G93/_new  2023-06-16 16:56:12.466190086 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-unearth
 #
-# Copyright (c) 2022 SUSE LLC
+# Copyright (c) 2023 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -16,8 +16,9 @@
 #
 
 
+%{?sle15_python_module_pythons}
 Name:           python-unearth
-Version:        0.7.0
+Version:        0.9.1
 Release:        0
 Summary:        A utility to fetch and download python packages
 License:        MIT

++++++ unearth-0.7.0.tar.gz -> unearth-0.9.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/PKG-INFO new/unearth-0.9.1/PKG-INFO
--- old/unearth-0.7.0/PKG-INFO  2022-12-12 06:10:18.483614400 +0100
+++ new/unearth-0.9.1/PKG-INFO  1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: unearth
-Version: 0.7.0
+Version: 0.9.1
 Summary: A utility to fetch and download python packages
 License: MIT
 Author-email: Frost Ming <m...@frostming.com>
@@ -65,7 +65,7 @@
 
 ```python
 >>> from unearth import PackageFinder
->>> finder = PackageFinder(index_urls=['https://pypi.org/simple/'])
+>>> finder = PackageFinder(index_urls=["https://pypi.org/simple/";])
 >>> result = finder.find_best_match("flask>=2")
 >>> result.best_candidate
 Package(name='flask', version='2.1.2', link=<Link 
https://files.pythonhosted.org/packages/ba/76/e9580e494eaf6f09710b0f3b9000c9c0363e44af5390be32bb0394165853/Flask-2.1.2-py3-none-any.whl#sha256=fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe
 (from https://pypi.org/simple/flask)>)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/README.md new/unearth-0.9.1/README.md
--- old/unearth-0.7.0/README.md 2022-12-12 06:10:02.951712000 +0100
+++ new/unearth-0.9.1/README.md 2023-05-15 09:34:42.539511400 +0200
@@ -43,7 +43,7 @@
 
 ```python
 >>> from unearth import PackageFinder
->>> finder = PackageFinder(index_urls=['https://pypi.org/simple/'])
+>>> finder = PackageFinder(index_urls=["https://pypi.org/simple/";])
 >>> result = finder.find_best_match("flask>=2")
 >>> result.best_candidate
 Package(name='flask', version='2.1.2', link=<Link 
https://files.pythonhosted.org/packages/ba/76/e9580e494eaf6f09710b0f3b9000c9c0363e44af5390be32bb0394165853/Flask-2.1.2-py3-none-any.whl#sha256=fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe
 (from https://pypi.org/simple/flask)>)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/pyproject.toml 
new/unearth-0.9.1/pyproject.toml
--- old/unearth-0.7.0/pyproject.toml    2022-12-12 06:10:02.955712000 +0100
+++ new/unearth-0.9.1/pyproject.toml    2023-05-15 09:34:42.539511400 +0200
@@ -29,7 +29,7 @@
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3 :: Only",
 ]
-version = "0.7.0"
+version = "0.9.1"
 
 [project.license]
 text = "MIT"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/__init__.py 
new/unearth-0.9.1/src/unearth/__init__.py
--- old/unearth-0.7.0/src/unearth/__init__.py   2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/__init__.py   2023-05-15 09:34:42.539511400 
+0200
@@ -7,12 +7,13 @@
 """
 from unearth.errors import HashMismatchError, UnpackError, URLError, 
VCSBackendError
 from unearth.evaluator import Package, TargetPython
-from unearth.finder import BestMatch, PackageFinder
+from unearth.finder import BestMatch, PackageFinder, Source
 from unearth.link import Link
 from unearth.vcs import vcs_support
 
 __all__ = [
     "Link",
+    "Source",
     "Package",
     "URLError",
     "BestMatch",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/__main__.py 
new/unearth-0.9.1/src/unearth/__main__.py
--- old/unearth-0.7.0/src/unearth/__main__.py   2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/__main__.py   2023-05-15 09:34:42.539511400 
+0200
@@ -8,10 +8,10 @@
 import sys
 import tempfile
 from dataclasses import dataclass
-from typing import cast
 
 from packaging.requirements import Requirement
 
+from unearth.evaluator import TargetPython
 from unearth.finder import PackageFinder
 from unearth.link import Link
 from unearth.utils import splitext
@@ -24,12 +24,16 @@
     index_urls: list[str]
     find_links: list[str]
     trusted_hosts: list[str]
-    no_binary: list[str]
-    only_binary: list[str]
+    no_binary: bool
+    only_binary: bool
     prefer_binary: bool
     all: bool
     link_only: bool
     download: str | None
+    py_ver: tuple[int, ...] | None
+    abis: list[str] | None
+    impl: str | None
+    platforms: list[str] | None
 
 
 def _setup_logger(verbosity: bool) -> None:
@@ -42,6 +46,14 @@
     logger.addHandler(handler)
 
 
+def comma_split(arg: str) -> list[str]:
+    return arg.split(",")
+
+
+def to_py_ver(arg: str) -> tuple[int, ...]:
+    return tuple(int(i) for i in arg.split(".") if i.isdigit())
+
+
 def cli_parser() -> argparse.ArgumentParser:
     parser = argparse.ArgumentParser(
         description="Find and download packages from a PEP 508 requirement 
string.",
@@ -57,8 +69,8 @@
     parser.add_argument(
         "--index-url",
         "-i",
-        dest="index_urls",
         metavar="URL",
+        dest="index_urls",
         action="append",
         help="(Multiple)(PEP 503)Simple Index URLs.",
     )
@@ -79,17 +91,13 @@
     )
     parser.add_argument(
         "--no-binary",
-        action="append",
-        metavar="PACKAGE",
-        help="(Multiple)Specify package names to exclude binary results, "
-        "or `:all:` to exclude all binary results.",
+        action="store_true",
+        help="Exclude binary packages from the results.",
     )
     parser.add_argument(
         "--only-binary",
-        action="append",
-        metavar="PACKAGE",
-        help="(Multiple)Specify package names to only allow binary results, "
-        "or `:all:` to enforce binary results for all packages.",
+        action="store_true",
+        help="Only include binary packages in the results.",
     )
     parser.add_argument(
         "--prefer-binary",
@@ -113,10 +121,34 @@
         metavar="DIR",
         help="Download the package(s) to DIR.",
     )
+    group = parser.add_argument_group("Target Python options")
+    group.add_argument(
+        "--python-version",
+        "--py",
+        dest="py_ver",
+        type=to_py_ver,
+        help="Target Python version. e.g. 3.11.0",
+    )
+    group.add_argument(
+        "--abis", type=comma_split, help="Comma-separated list of ABIs. e.g. 
cp39,cp310"
+    )
+    group.add_argument(
+        "--implementation",
+        "--impl",
+        dest="impl",
+        help="Python implementation. e.g. cp,pp,jy,ip",
+    )
+    group.add_argument(
+        "--platforms",
+        type=comma_split,
+        help="Comma-separated list of platforms. e.g. win_amd64,linux_x86_64",
+    )
     return parser
 
 
 def get_dest_for_package(dest: str, link: Link) -> str:
+    if link.is_wheel:
+        return dest
     filename = link.filename.rsplit("@", 1)[0]
     fn, _ = splitext(filename)
     return os.path.join(dest, fn)
@@ -124,18 +156,21 @@
 
 def cli(argv: list[str] | None = None) -> None:
     parser = cli_parser()
-    args = cast(CLIArgs, parser.parse_args(argv))
+    args = CLIArgs(**vars(parser.parse_args(argv)))
     _setup_logger(args.verbose)
+    name = args.requirement.name
+    target_python = TargetPython(args.py_ver, args.abis, args.impl, 
args.platforms)
     finder = PackageFinder(
-        index_urls=args.index_urls or ["https://pypi.org/simple";],
+        index_urls=args.index_urls or ["https://pypi.org/simple/";],
         find_links=args.find_links or [],
         trusted_hosts=args.trusted_hosts or [],
-        no_binary=args.no_binary or [],
-        only_binary=args.only_binary or [],
-        prefer_binary=args.prefer_binary,
+        target_python=target_python,
+        no_binary=[name] if args.no_binary else [],
+        only_binary=[name] if args.only_binary else [],
+        prefer_binary=[name] if args.prefer_binary else [],
         verbosity=int(args.verbose),
     )
-    matches = finder.find_matches(args.requirement)
+    matches = list(finder.find_matches(args.requirement))
     if not matches:
         print("No matches are found.", file=sys.stderr)
         sys.exit(1)
@@ -143,15 +178,13 @@
         matches = matches[:1]
 
     result = []
-    with tempfile.TemporaryDirectory("unearth-download-") as tempdir:
+    if args.download:
+        os.makedirs(args.download, exist_ok=True)
+    with tempfile.TemporaryDirectory("unearth-download-") as download_dir:
         for match in matches:
             data = match.as_json()
             if args.download is not None:
-                download_dir, dest = tempdir, get_dest_for_package(
-                    args.download, match.link
-                )
-                if match.link.is_wheel:
-                    download_dir = args.download
+                dest = get_dest_for_package(args.download, match.link)
                 data["local_path"] = finder.download_and_unpack(
                     match.link,
                     dest,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/auth.py 
new/unearth-0.9.1/src/unearth/auth.py
--- old/unearth-0.7.0/src/unearth/auth.py       2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/auth.py       2023-05-15 09:34:42.539511400 
+0200
@@ -1,7 +1,11 @@
 from __future__ import annotations
 
+import abc
 import getpass
 import logging
+import os
+import shutil
+import subprocess
 from typing import Any, Iterable, Optional, Tuple, cast
 from urllib.parse import urlparse
 
@@ -12,48 +16,134 @@
 
 from unearth.utils import split_auth_from_netloc, split_auth_from_url
 
-try:
-    import keyring  # type: ignore
-except ModuleNotFoundError:
-    keyring = None  # type: ignore[assignment]
+KEYRING_DISABLED = False
 
 AuthInfo = Tuple[str, str]
 MaybeAuth = Optional[Tuple[str, Optional[str]]]
 logger = logging.getLogger(__name__)
 
 
-def get_keyring_auth(url: str | None, username: str | None) -> AuthInfo | None:
-    """Return the tuple auth for a given url from keyring."""
-    global keyring
-    if not url or not keyring:
-        return None
+class KeyringBaseProvider(metaclass=abc.ABCMeta):
+    """Base class for keyring providers."""
 
-    try:
-        try:
-            get_credential = keyring.get_credential
-        except AttributeError:
-            pass
-        else:
-            logger.debug("Getting credentials from keyring for %s", url)
-            cred = get_credential(url, username)
+    @abc.abstractmethod
+    def get_auth_info(self, url: str, username: str | None) -> AuthInfo | None:
+        """Return the password for the given url and username.
+        The username can be None.
+        """
+        ...
+
+    @abc.abstractmethod
+    def save_auth_info(self, url: str, username: str, password: str) -> None:
+        """Set the password for the given url and username."""
+        ...
+
+
+class KeyringModuleProvider(KeyringBaseProvider):
+    """Keyring provider that uses the keyring module."""
+
+    def __init__(self) -> None:
+        import keyring  # type: ignore
+
+        self.keyring = keyring
+
+    def get_auth_info(self, url: str, username: str | None) -> AuthInfo | None:
+        if hasattr(self.keyring, "get_credential"):
+            logger.debug("Getting credentials from keyring for url: %s", url)
+            cred = self.keyring.get_credential(url, username)
             if cred is not None:
                 return cred.username, cred.password
-            return None
 
-        if not username:
+        if username is None:
             username = "__token__"
-        logger.debug("Getting password from keyring for %s@%s", username, url)
-        password = keyring.get_password(url, username)
+        logger.debug("Getting password from keyring for: %s@%s", username, url)
+        password = self.keyring.get_password(url, username)
         if password:
             return username, password
+        return None
+
+    def save_auth_info(self, url: str, username: str, password: str) -> None:
+        self.keyring.set_password(url, username, password)
+
+
+class KeyringCliProvider(KeyringBaseProvider):
+    def __init__(self, cmd: str) -> None:
+        self.keyring = cmd
+
+    def get_auth_info(self, url: str, username: str | None) -> AuthInfo | None:
+        if username is not None:
+            logger.debug("Getting password from keyring CLI for %s@%s", 
username, url)
+            password = self._get_password(url, username)
+            if password is not None:
+                return username, password
+        return None
+
+    def save_auth_info(self, url: str, username: str, password: str) -> None:
+        return self._set_password(url, username, password)
+
+    def _get_password(self, service_name: str, username: str) -> str | None:
+        """Mirror the implementation of keyring.get_password using cli"""
+        cmd = [self.keyring, "get", service_name, username]
+        env = dict(os.environ, PYTHONIOENCODING="utf-8")
+        res = subprocess.run(
+            cmd, stdin=subprocess.DEVNULL, capture_output=True, env=env
+        )
+        if res.returncode:
+            return None
+        return res.stdout.decode("utf-8").strip(os.linesep)
 
+    def _set_password(self, service_name: str, username: str, password: str) 
-> None:
+        """Mirror the implementation of keyring.set_password using cli"""
+        if self.keyring is None:
+            return None
+
+        cmd = [self.keyring, "set", service_name, username]
+        input_ = (password + os.linesep).encode("utf-8")
+        env = dict(os.environ, PYTHONIOENCODING="utf-8")
+        subprocess.run(cmd, input=input_, env=env, check=True)
+        return None
+
+
+def get_keyring_provider() -> KeyringBaseProvider | None:
+    """Return the keyring provider to use."""
+    if KEYRING_DISABLED:
+        return None
+
+    try:
+        return KeyringModuleProvider()
+    except ImportError:
+        pass
+    except Exception as exc:
+        logger.warning(
+            "Importing keyring failed: %s, trying to find a keyring 
executable.",
+            exc,
+        )
+
+    keyring = shutil.which("keyring")
+    if keyring is not None:
+        return KeyringCliProvider(keyring)
+
+    return None
+
+
+def get_keyring_auth(url: str | None, username: str | None) -> AuthInfo | None:
+    """Return the tuple auth for a given url from keyring."""
+    if not url:
+        return None
+
+    keyring = get_keyring_provider()
+    if keyring is None:
+        return None
+    try:
+        return keyring.get_auth_info(url, username)
     except Exception as exc:
         logger.warning(
             "Keyring is skipped due to an exception: %s",
             str(exc),
         )
-        keyring = None  # type: ignore[assignment]
-    return None
+        global KEYRING_DISABLED
+        KEYRING_DISABLED = True
+        return None
 
 
 class MultiDomainBasicAuth(AuthBase):
@@ -190,7 +280,7 @@
 
     # Factored out to allow for easy patching in tests
     def _should_save_password_to_keyring(self) -> bool:
-        if not keyring:
+        if get_keyring_provider() is None:
             return False
         return input("Save credentials to keyring [y/N]: ") == "y"
 
@@ -260,15 +350,14 @@
 
     def save_credentials(self, resp: Response, **kwargs: Any) -> None:
         """Response callback to save credentials on success."""
+        keyring = get_keyring_provider()
         assert keyring is not None, "should never reach here without keyring"
-        if not keyring:
-            return
 
         creds = self._credentials_to_save
         self._credentials_to_save = None
         if creds and resp.status_code < 400:
             try:
                 logger.info("Saving credentials to keyring")
-                keyring.set_password(*creds)
+                keyring.save_auth_info(*creds)
             except Exception:
                 logger.exception("Failed to save credentials")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/collector.py 
new/unearth-0.9.1/src/unearth/collector.py
--- old/unearth-0.7.0/src/unearth/collector.py  2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/collector.py  2023-05-15 09:34:42.539511400 
+0200
@@ -90,7 +90,7 @@
         requires_python: str | None = file.get("requires-python")
         yank_reason: str | None = file.get("yanked") or None
         dist_info_metadata: bool | dict[str, str] | None = file.get(
-            "dist-info-metadata"
+            "data-dist-info-metadata"
         )
         hashes: dict[str, str] | None = file.get("hashes")
         yield Link(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/evaluator.py 
new/unearth-0.9.1/src/unearth/evaluator.py
--- old/unearth-0.7.0/src/unearth/evaluator.py  2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/evaluator.py  2023-05-15 09:34:42.539511400 
+0200
@@ -9,7 +9,7 @@
 from urllib.parse import urlencode
 
 import packaging.requirements
-from packaging.specifiers import SpecifierSet
+from packaging.specifiers import InvalidSpecifier, SpecifierSet
 from packaging.tags import Tag
 from packaging.utils import (
     InvalidWheelFilename,
@@ -150,7 +150,13 @@
         if not self.ignore_compatibility and link.requires_python:
             py_ver = self.target_python.py_ver or sys.version_info[:2]
             py_version = ".".join(str(v) for v in py_ver)
-            if not SpecifierSet(link.requires_python).contains(py_version, 
True):
+            try:
+                requires_python = SpecifierSet(link.requires_python)
+            except InvalidSpecifier:
+                raise LinkMismatchError(
+                    f"Invalid requires-python: {link.requires_python}"
+                )
+            if not requires_python.contains(py_version, True):
                 raise LinkMismatchError(
                     "The target python version({}) doesn't match "
                     "the requires-python specifier {}".format(
@@ -249,7 +255,7 @@
                     )
             self._check_hashes(link)
         except LinkMismatchError as e:
-            logger.debug("Skip link %s: %s", link, e)
+            logger.debug("Skipping link %s: %s", link, e)
             return None
         return Package(name=self.package_name, version=version, link=link)
 
@@ -272,18 +278,17 @@
     if requirement.name:
         if canonicalize_name(package.name) != 
canonicalize_name(requirement.name):
             logger.debug(
-                "Skip package %s: name doesn't match %s", package, 
requirement.name
+                "Skipping package %s: name doesn't match %s", package, 
requirement.name
             )
             return False
 
-    if requirement.specifier and package.version:
-        if not requirement.specifier.contains(
-            package.version, prereleases=allow_prereleases
-        ):
-            logger.debug(
-                "Skip package %s: version doesn't match %s",
-                package,
-                requirement.specifier,
-            )
-            return False
+    if package.version and not requirement.specifier.contains(
+        package.version, prereleases=allow_prereleases
+    ):
+        logger.debug(
+            "Skipping package %s: version doesn't match %s",
+            package,
+            requirement.specifier,
+        )
+        return False
     return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/finder.py 
new/unearth-0.9.1/src/unearth/finder.py
--- old/unearth-0.7.0/src/unearth/finder.py     2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/finder.py     2023-05-15 09:34:42.539511400 
+0200
@@ -3,10 +3,11 @@
 
 import atexit
 import functools
+import itertools
 import os
 import pathlib
 from tempfile import TemporaryDirectory
-from typing import Iterable, NamedTuple
+from typing import TYPE_CHECKING, Iterable, NamedTuple, Sequence
 from urllib.parse import urljoin
 
 import packaging.requirements
@@ -25,7 +26,17 @@
 from unearth.link import Link
 from unearth.preparer import unpack_link
 from unearth.session import PyPISession
-from unearth.utils import split_auth_from_url
+from unearth.utils import LazySequence
+
+if TYPE_CHECKING:
+    from typing import TypedDict
+
+    class Source(TypedDict):
+        url: str
+        type: str
+
+else:
+    Source = dict
 
 
 class BestMatch(NamedTuple):
@@ -34,9 +45,9 @@
     #: The best matching package, or None if no match was found.
     best: Package | None
     #: The applicable packages, excluding those with unmatching versions.
-    applicable: list[Package]
+    applicable: Sequence[Package]
     #: All candidates found for the requirement.
-    candidates: list[Package]
+    candidates: Sequence[Package]
 
 
 class PackageFinder:
@@ -45,16 +56,16 @@
     Args:
         session (PyPISession|None): The session to use for the finder.
             If not provided, a temporary session will be created.
-        index_urls: (Iterable[str]): The urls of the index pages.
-        find_links: (Iterable[str]): The urls or paths of the find links.
+        index_urls (Iterable[str]): The index URLs to search for packages.
+        find_links (Iterable[str]): The links to search for packages.
         trusted_hosts: (Iterable[str]): The trusted hosts.
         target_python (TargetPython): The links must match
             the target Python
         ignore_compatibility (bool): Whether to ignore the compatibility check
         no_binary (Iterable[str]): The names of the packages to disallow wheels
         only_binary (Iterable[str]): The names of the packages to disallow 
non-wheels
-        prefer_binary (bool): Whether to prefer binary packages even if
-            newer sdist pacakges exist.
+        prefer_binary (Iterable[str]): The names of the packages to prefer 
binary
+            distributions even if newer sdist pacakges exist.
         respect_source_order (bool): If True, packages from the source coming 
earlier
             are more preferred, even if they have lower versions.
         verbosity (int): The verbosity level.
@@ -63,6 +74,7 @@
     def __init__(
         self,
         session: PyPISession | None = None,
+        *,
         index_urls: Iterable[str] = (),
         find_links: Iterable[str] = (),
         trusted_hosts: Iterable[str] = (),
@@ -70,33 +82,57 @@
         ignore_compatibility: bool = False,
         no_binary: Iterable[str] = (),
         only_binary: Iterable[str] = (),
-        prefer_binary: bool = False,
+        prefer_binary: Iterable[str] = (),
         respect_source_order: bool = False,
         verbosity: int = 0,
     ) -> None:
-        self.index_urls = list(index_urls)
-        self.find_links = list(find_links)
+        self.sources: list[Source] = []
+        for url in index_urls:
+            self.add_index_url(url)
+        for url in find_links:
+            self.add_find_links(url)
         self.target_python = target_python or TargetPython()
         self.ignore_compatibility = ignore_compatibility
         self.no_binary = [canonicalize_name(name) for name in no_binary]
         self.only_binary = [canonicalize_name(name) for name in only_binary]
-        self.prefer_binary = prefer_binary
-        if session is None:
-            session = PyPISession(
-                index_urls=self.index_urls, trusted_hosts=trusted_hosts
-            )
-            atexit.register(session.close)
-        self.session = session
+        self.prefer_binary = [canonicalize_name(name) for name in 
prefer_binary]
+        self.trusted_hosts = trusted_hosts
+        self._session = session
         self.respect_source_order = respect_source_order
         self.verbosity = verbosity
 
         self._tag_priorities = {
             tag: i for i, tag in enumerate(self.target_python.supported_tags())
         }
-        # Index pages are preferred over find links.
-        self._source_order = [
-            split_auth_from_url(url)[1] for url in (self.index_urls + 
self.find_links)
-        ]
+
+    @property
+    def session(self) -> PyPISession:
+        if self._session is None:
+            index_urls = [
+                source["url"] for source in self.sources if source["type"] == 
"index"
+            ]
+            session = PyPISession(
+                index_urls=index_urls, trusted_hosts=self.trusted_hosts
+            )
+            atexit.register(session.close)
+            self._session = session
+        return self._session
+
+    def add_index_url(self, url: str) -> None:
+        """Add an index URL to the finder search scope.
+
+        Args:
+            url (str): The index URL to add.
+        """
+        self.sources.append({"url": url, "type": "index"})
+
+    def add_find_links(self, url: str) -> None:
+        """Add a find links URL to the finder search scope.
+
+        Args:
+            url (str): The find links URL to add.
+        """
+        self.sources.append({"url": url, "type": "find_links"})
 
     def build_evaluator(
         self,
@@ -140,7 +176,9 @@
     def _build_find_link(self, find_link: str) -> Link:
         if os.path.exists(find_link):
             return Link.from_path(os.path.abspath(find_link))
-        return Link(find_link)
+        elif "://" in find_link:
+            return Link(find_link)
+        raise ValueError(f"Invalid find link or non-existing path: 
{find_link}")
 
     def _evaluate_links(
         self, links: Iterable[Link], evaluator: Evaluator
@@ -163,32 +201,21 @@
     def _sort_key(self, package: Package) -> tuple:
         """The key for sort, package with the largest value is the most 
preferred."""
         link = package.link
-        pri = len(self._tag_priorities)
+        pri = len(self._tag_priorities) + 1  # default priority for sdist.
         build_tag: BuildTag = ()
         prefer_binary = False
         if link.is_wheel:
             *_, build_tag, file_tags = parse_wheel_filename(link.filename)
             pri = min(
-                (self._tag_priorities.get(tag, pri) for tag in file_tags), 
default=pri
+                (self._tag_priorities.get(tag, pri - 1) for tag in file_tags),
+                default=pri - 1,
             )
-            if self.prefer_binary:
+            if canonicalize_name(package.name) in self.prefer_binary:
                 prefer_binary = True
-        comes_from = package.link.comes_from
-        source_index = len(self._source_order)
 
-        if comes_from is not None and self.respect_source_order:
-            source_index = next(
-                (
-                    i
-                    for i, url in enumerate(self._source_order)
-                    if comes_from.startswith(url)
-                ),
-                source_index,
-            )
         return (
             -int(link.is_yanked),
             int(prefer_binary),
-            -source_index,
             parse_version(package.version) if package.version is not None else 
0,
             -pri,
             build_tag,
@@ -208,26 +235,39 @@
             hashes (dict[str, list[str]]|None): The hashes to filter on.
 
         Returns:
-            Iterable[Package]: The packages with the given name
+            Iterable[Package]: The packages with the given name, sorted by 
best match.
         """
         evaluator = self.build_evaluator(package_name, allow_yanked, hashes)
-        for index_url in self.index_urls:
-            package_link = self._build_index_page_link(index_url, package_name)
-            yield from self._evaluate_links(
-                collect_links_from_location(self.session, package_link), 
evaluator
-            )
-        for find_link in self.find_links:
-            link = self._build_find_link(find_link)
-            yield from self._evaluate_links(
-                collect_links_from_location(self.session, link, expand=True), 
evaluator
-            )
+
+        def find_one_source(source: Source) -> Iterable[Package]:
+            if source["type"] == "index":
+                link = self._build_index_page_link(source["url"], package_name)
+                result = self._evaluate_links(
+                    collect_links_from_location(self.session, link), evaluator
+                )
+            else:
+                link = self._build_find_link(source["url"])
+                result = self._evaluate_links(
+                    collect_links_from_location(self.session, link, 
expand=True),
+                    evaluator,
+                )
+            if self.respect_source_order:
+                # Sort the result within the individual source.
+                return sorted(result, key=self._sort_key, reverse=True)
+            return result
+
+        all_packages = itertools.chain.from_iterable(map(find_one_source, 
self.sources))
+        if self.respect_source_order:
+            return all_packages
+        # Otherwise, sort the result across all sources.
+        return sorted(all_packages, key=self._sort_key, reverse=True)
 
     def find_all_packages(
         self,
         package_name: str,
         allow_yanked: bool = False,
         hashes: dict[str, list[str]] | None = None,
-    ) -> list[Package]:
+    ) -> Sequence[Package]:
         """Find all packages with the given package name, best match first.
 
         Args:
@@ -236,13 +276,9 @@
             hashes (dict[str, list[str]]|None): The hashes to filter on.
 
         Returns:
-            list[Package]: The packages list sorted by best match
+            Sequence[Package]: The packages list sorted by best match
         """
-        return sorted(
-            self._find_packages(package_name, allow_yanked, hashes),
-            key=self._sort_key,
-            reverse=True,
-        )
+        return LazySequence(self._find_packages(package_name, allow_yanked, 
hashes))
 
     def _find_packages_from_requirement(
         self,
@@ -263,7 +299,7 @@
         allow_yanked: bool | None = None,
         allow_prereleases: bool | None = None,
         hashes: dict[str, list[str]] | None = None,
-    ) -> list[Package]:
+    ) -> Sequence[Package]:
         """Find all packages matching the given requirement, best match first.
 
         Args:
@@ -276,18 +312,16 @@
             hashes (dict[str, list[str]]|None): The hashes to filter on.
 
         Returns:
-            list[Package]: The packages list sorted by best match
+            Sequence[Package]: The packages sorted by best match
         """
         if isinstance(requirement, str):
             requirement = packaging.requirements.Requirement(requirement)
-        return sorted(
+        return LazySequence(
             self._evaluate_packages(
                 self._find_packages_from_requirement(requirement, 
allow_yanked, hashes),
                 requirement,
                 allow_prereleases,
-            ),
-            key=self._sort_key,
-            reverse=True,
+            )
         )
 
     def find_best_match(
@@ -313,13 +347,14 @@
         """
         if isinstance(requirement, str):
             requirement = packaging.requirements.Requirement(requirement)
-        candidates = list(
-            self._find_packages_from_requirement(requirement, allow_yanked, 
hashes)
+        packages = self._find_packages_from_requirement(
+            requirement, allow_yanked, hashes
         )
-        applicable_candidates = list(
-            self._evaluate_packages(candidates, requirement, allow_prereleases)
+        candidates = LazySequence(packages)
+        applicable_candidates = LazySequence(
+            self._evaluate_packages(packages, requirement, allow_prereleases)
         )
-        best_match = max(applicable_candidates, key=self._sort_key, 
default=None)
+        best_match = next(iter(applicable_candidates), None)
         return BestMatch(best_match, applicable_candidates, candidates)
 
     def download_and_unpack(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/session.py 
new/unearth-0.9.1/src/unearth/session.py
--- old/unearth-0.7.0/src/unearth/session.py    2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/session.py    2023-05-15 09:34:42.539511400 
+0200
@@ -193,7 +193,7 @@
             return True
 
         logger.warning(
-            "Skip %s for not being trusted, please add it to `trusted_hosts` 
list",
+            "Skipping %s for not being trusted, please add it to 
`trusted_hosts` list",
             location.redacted,
         )
         return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/utils.py 
new/unearth-0.9.1/src/unearth/utils.py
--- old/unearth-0.7.0/src/unearth/utils.py      2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/utils.py      2023-05-15 09:34:42.539511400 
+0200
@@ -2,10 +2,12 @@
 from __future__ import annotations
 
 import functools
+import itertools
 import os
 import sys
 import urllib.parse as parse
 from pathlib import Path
+from typing import Iterable, Iterator, Sequence, TypeVar
 from urllib.request import pathname2url, url2pathname
 
 WINDOWS = sys.platform == "win32"
@@ -126,8 +128,8 @@
     auth, has_auth, host = netloc.rpartition("@")
     if not has_auth:
         return None, host
-    user, _, password = auth.partition(":")
-    return (parse.unquote(user), parse.unquote(password) if password else 
None), host
+    user, has_pass, password = auth.partition(":")
+    return (parse.unquote(user), parse.unquote(password) if has_pass else 
None), host
 
 
 @functools.lru_cache(maxsize=128)
@@ -182,3 +184,36 @@
         return f"{int_size / 1000.0:.1f} kB"
     else:
         return f"{int(int_size)} bytes"
+
+
+T = TypeVar("T", covariant=True)
+
+
+class LazySequence(Sequence[T]):
+    """A sequence that is lazily evaluated."""
+
+    def __init__(self, data: Iterable[T]) -> None:
+        self._inner = data
+
+    def __iter__(self) -> Iterator[T]:
+        self._inner, this = itertools.tee(self._inner)
+        return this
+
+    def __len__(self) -> int:
+        i = 0
+        for _ in self:
+            i += 1
+        return i
+
+    def __bool__(self) -> bool:
+        for _ in self:
+            return True
+        return False
+
+    def __getitem__(self, index: int) -> T:  # type: ignore[override]
+        if index < 0:
+            raise IndexError("Negative indices are not supported")
+        for i, item in enumerate(self):
+            if i == index:
+                return item
+        raise IndexError("Index out of range")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/src/unearth/vcs/git.py 
new/unearth-0.9.1/src/unearth/vcs/git.py
--- old/unearth-0.7.0/src/unearth/vcs/git.py    2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/src/unearth/vcs/git.py    2023-05-15 09:34:42.539511400 
+0200
@@ -35,8 +35,10 @@
     ) -> None:
         rev_display = f" (revision: {rev})" if rev else ""
         logger.info("Cloning %s%s to %s", url, rev_display, 
display_path(location))
+        env = None
         if self.verbosity <= 0:
             flags: tuple[str, ...] = ("--quiet",)
+            env = {"GIT_TERMINAL_PROMPT": "0"}
         elif self.verbosity == 1:
             flags = ()
         else:
@@ -47,9 +49,10 @@
             # Speeds up cloning by functioning without a complete copy of 
repository
             self.run_command(
                 ["clone", "--filter=blob:none", *flags, url, str(location)],
+                extra_env=env,
             )
         else:
-            self.run_command(["clone", *flags, url, str(location)])
+            self.run_command(["clone", *flags, url, str(location)], 
extra_env=env)
 
         if rev is not None:
             self.run_command(["checkout", "-q", rev], cwd=location)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/tests/conftest.py 
new/unearth-0.9.1/tests/conftest.py
--- old/unearth-0.7.0/tests/conftest.py 2022-12-12 06:10:02.955712000 +0100
+++ new/unearth-0.9.1/tests/conftest.py 2023-05-15 09:34:42.539511400 +0200
@@ -1,4 +1,5 @@
 """Configuration for the pytest test suite."""
+import os
 from ssl import SSLContext
 from unittest import mock
 
@@ -71,7 +72,9 @@
 @pytest.fixture()
 def pypi_auth(pypi):
     def check_auth(auth):
-        return auth.username == "test" and auth.password == "password"
+        return auth.username == os.getenv(
+            "PYPI_USER", "test"
+        ) and auth.password == os.getenv("PYPI_PASSWORD", "password")
 
     def unauthenticated():
         message = {"message": "Unauthenticated"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/tests/test_evaluator.py 
new/unearth-0.9.1/tests/test_evaluator.py
--- old/unearth-0.7.0/tests/test_evaluator.py   2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/tests/test_evaluator.py   2023-05-15 09:34:42.543511400 
+0200
@@ -249,6 +249,7 @@
         ("8.0.0a0", ">=8.0.0dev0", None, True),
         ("8.0.0dev0", ">=7", None, False),
         ("8.0.0dev0", ">=7", True, True),
+        ("8.0.0a0", "", None, False),
         ("8.0.0a0", ">=8.0.0dev0", False, False),
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/tests/test_finder.py 
new/unearth-0.9.1/tests/test_finder.py
--- old/unearth-0.7.0/tests/test_finder.py      2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/tests/test_finder.py      2023-05-15 09:34:42.543511400 
+0200
@@ -6,6 +6,8 @@
 
 pytestmark = pytest.mark.usefixtures("pypi", "content_type")
 
+DEFAULT_INDEX_URL = "https://pypi.org/simple/";
+
 
 @pytest.mark.parametrize(
     "target_python,filename",
@@ -34,15 +36,15 @@
 )
 def test_find_most_matching_wheel(session, target_python, filename):
     finder = PackageFinder(
-        session, index_urls=["https://pypi.org/simple";], 
target_python=target_python
+        session=session, index_urls=[DEFAULT_INDEX_URL], 
target_python=target_python
     )
     assert finder.find_best_match("black").best.link.filename == filename
 
 
 def test_find_package_with_format_control(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         target_python=TargetPython(
             (3, 9), abis=["cp39"], impl="cp", platforms=["win_amd64"]
         ),
@@ -58,8 +60,8 @@
 
 def test_find_package_no_binary_for_all(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         target_python=TargetPython(
             (3, 9), abis=["cp39"], impl="cp", platforms=["win_amd64"]
         ),
@@ -71,12 +73,12 @@
 
 def test_find_package_prefer_binary(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         target_python=TargetPython(
             (3, 9), abis=["cp39"], impl="cp", platforms=["win_amd64"]
         ),
-        prefer_binary=True,
+        prefer_binary=["first"],
     )
     assert (
         finder.find_best_match("first").best.link.filename
@@ -86,8 +88,8 @@
 
 def test_find_package_with_hash_allowance(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         target_python=TargetPython(
             (3, 9), abis=["cp39"], impl="cp", platforms=["win_amd64"]
         ),
@@ -108,8 +110,8 @@
 @pytest.mark.parametrize("ignore_compat", [True, False])
 def test_find_package_ignoring_compatibility(session, ignore_compat):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         target_python=TargetPython(
             (3, 9), abis=["cp39"], impl="cp", platforms=["win_amd64"]
         ),
@@ -121,8 +123,8 @@
 
 def test_find_package_with_version_specifier(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         ignore_compatibility=True,
     )
     matches = finder.find_matches("black==22.3.0")
@@ -134,8 +136,8 @@
 
 def test_find_package_allowing_prereleases(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         ignore_compatibility=True,
     )
     matches = finder.find_matches("black<22.3.0", allow_prereleases=True)
@@ -152,8 +154,8 @@
 
 def test_find_requirement_with_link(session):
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         ignore_compatibility=True,
     )
     req = "first @ https://pypi.org/files/first-2.0.2.tar.gz";
@@ -166,11 +168,9 @@
 def test_find_requirement_preference(session, fixtures_dir):
     find_link = Link.from_path(fixtures_dir / "findlinks/index.html")
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
-        find_links=[find_link.normalized],
-        ignore_compatibility=True,
+        session=session, index_urls=[DEFAULT_INDEX_URL], 
ignore_compatibility=True
     )
+    finder.add_find_links(find_link.normalized)
     best = finder.find_best_match("first").best
     assert best.link.filename == "first-2.0.3-py2.py3-none-any.whl"
     assert best.link.comes_from == find_link.normalized
@@ -179,12 +179,12 @@
 def test_find_requirement_preference_respect_source_order(session, 
fixtures_dir):
     find_link = Link.from_path(fixtures_dir / "findlinks/index.html")
     finder = PackageFinder(
-        session,
-        index_urls=["https://pypi.org/simple";],
-        find_links=[find_link.normalized],
+        session=session,
+        index_urls=[DEFAULT_INDEX_URL],
         ignore_compatibility=True,
         respect_source_order=True,
     )
+    finder.add_find_links(find_link.normalized)
     best = finder.find_best_match("first").best
     assert best.link.filename == "first-2.0.2.tar.gz"
     assert best.link.comes_from == "https://pypi.org/simple/first/";
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/tests/test_session.py 
new/unearth-0.9.1/tests/test_session.py
--- old/unearth-0.7.0/tests/test_session.py     2022-12-12 06:10:02.955712000 
+0100
+++ new/unearth-0.9.1/tests/test_session.py     2023-05-15 09:34:42.543511400 
+0200
@@ -57,7 +57,17 @@
     assert not any(r.status_code == 401 for r in resp.history)
 
 
-def test_session_auth_from_prompting(pypi_auth, session, monkeypatch):
+def test_session_auth_with_empty_password(pypi_auth, session, monkeypatch):
+    monkeypatch.setenv("PYPI_PASSWORD", "")
+    session.auth = MultiDomainBasicAuth(
+        prompting=False, index_urls=["https://test:@pypi.org/simple";]
+    )
+    resp = session.get("https://pypi.org/simple/click";)
+    assert resp.status_code == 200
+    assert not any(r.status_code == 401 for r in resp.history)
+
+
+def test_session_auth_from_prompting(pypi_auth, session):
     with mock.patch.object(
         MultiDomainBasicAuth,
         "_prompt_for_password",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.7.0/tests/test_utils.py 
new/unearth-0.9.1/tests/test_utils.py
--- old/unearth-0.7.0/tests/test_utils.py       1970-01-01 01:00:00.000000000 
+0100
+++ new/unearth-0.9.1/tests/test_utils.py       2023-05-15 09:34:42.543511400 
+0200
@@ -0,0 +1,25 @@
+from unittest import mock
+
+from unearth.utils import LazySequence
+
+
+def test_lazy_sequence():
+    func = mock.Mock()
+
+    def gen(size):
+        for i in range(size):
+            func()
+            yield i
+
+    seq = LazySequence(gen(5))
+    assert bool(seq) is True
+    assert func.call_count == 1
+    assert seq[0] == 0
+    assert func.call_count == 1
+    assert seq[1] == 1
+    assert func.call_count == 2
+    assert 3 in seq
+    assert func.call_count == 4
+    assert len(seq) == 5
+    assert list(seq) == [0, 1, 2, 3, 4]
+    assert func.call_count == 5

Reply via email to