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 2026-08-18 16:38:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-unearth (Old)
 and      /work/SRC/openSUSE:Factory/.python-unearth.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-unearth"

Tue Aug 18 16:38:28 2026 rev:13 rq:1371773 version:0.18.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-unearth/python-unearth.changes    
2026-02-09 11:44:19.812195987 +0100
+++ /work/SRC/openSUSE:Factory/.python-unearth.new.1258/python-unearth.changes  
2026-08-18 16:39:05.653537053 +0200
@@ -1,0 +2,12 @@
+Tue Aug 18 10:56:55 UTC 2026 - Nico Krapp <[email protected]>
+
+- Update to 0.18.3 (fixes CVE-2026-73030 (bsc#1275440))
+  * cli: Download packages without unpacking
+  * Update actions/checkout and setup actions to v6
+  * Support packaging 26.0 changes
+  * Prevent tar-slip via path traversal and symlink escape in _untar_archive
+  * Complete path traversal protection by replacing os.makedirs with
+    safe_makedirs (CVE-2026-73030)
+- drop support-packaging-26.patch, merged upstream
+
+-------------------------------------------------------------------

Old:
----
  support-packaging-26.patch
  unearth-0.18.2.tar.gz

New:
----
  unearth-0.18.3.tar.gz

----------(Old B)----------
  Old:    safe_makedirs (CVE-2026-73030)
- drop support-packaging-26.patch, merged upstream
----------(Old E)----------

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

Other differences:
------------------
++++++ python-unearth.spec ++++++
--- /var/tmp/diff_new_pack.vNnvmq/_old  2026-08-18 16:39:06.930582753 +0200
+++ /var/tmp/diff_new_pack.vNnvmq/_new  2026-08-18 16:39:06.932582825 +0200
@@ -18,14 +18,12 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-unearth
-Version:        0.18.2
+Version:        0.18.3
 Release:        0
 Summary:        A utility to fetch and download python packages
 License:        MIT
 URL:            https://unearth.readthedocs.io/
 Source:         
https://files.pythonhosted.org/packages/source/u/unearth/unearth-%{version}.tar.gz
-# PATCH-FIX-UPSTREAM gh#frostming/unearth#176
-Patch0:         support-packaging-26.patch
 BuildRequires:  %{python_module base >= 3.9}
 BuildRequires:  %{python_module packaging >= 20}
 BuildRequires:  %{python_module pdm-backend}

++++++ unearth-0.18.2.tar.gz -> unearth-0.18.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/PKG-INFO new/unearth-0.18.3/PKG-INFO
--- old/unearth-0.18.2/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/unearth-0.18.3/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: unearth
-Version: 0.18.2
+Version: 0.18.3
 Summary: A utility to fetch and download python packages
 Author-Email: Frost Ming <[email protected]>
 License-Expression: MIT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/pyproject.toml 
new/unearth-0.18.3/pyproject.toml
--- old/unearth-0.18.2/pyproject.toml   2025-12-23 07:40:14.274020000 +0100
+++ new/unearth-0.18.3/pyproject.toml   2026-08-14 06:13:52.676418800 +0200
@@ -33,7 +33,7 @@
     "Programming Language :: Python :: 3.14",
     "Programming Language :: Python :: 3 :: Only",
 ]
-version = "0.18.2"
+version = "0.18.3"
 
 [project.urls]
 Homepage = "https://github.com/frostming/unearth";
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/__main__.py 
new/unearth-0.18.3/src/unearth/__main__.py
--- old/unearth-0.18.2/src/unearth/__main__.py  2025-12-23 07:40:02.004991500 
+0100
+++ new/unearth-0.18.3/src/unearth/__main__.py  2026-08-14 06:13:27.665497800 
+0200
@@ -7,15 +7,12 @@
 import logging
 import os
 import sys
-import tempfile
 from dataclasses import dataclass
 
 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
 
 
 @dataclass(frozen=True)
@@ -147,14 +144,6 @@
     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)
-
-
 def cli(argv: list[str] | None = None) -> None:
     parser = cli_parser()
     args = CLIArgs(**vars(parser.parse_args(argv)))
@@ -181,17 +170,14 @@
     result = []
     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:
-                dest = get_dest_for_package(args.download, match.link)
-                data["local_path"] = finder.download_and_unpack(
-                    match.link,
-                    dest,
-                    download_dir,
-                ).as_posix()
-            result.append(data)
+    for match in matches:
+        data = match.as_json()
+        if args.download is not None:
+            data["local_path"] = finder.download(
+                match.link,
+                args.download,
+            ).as_posix()
+        result.append(data)
     if args.link_only:
         for item in result:
             print(item["link"]["url"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/auth.py 
new/unearth-0.18.3/src/unearth/auth.py
--- old/unearth-0.18.2/src/unearth/auth.py      2025-12-23 07:40:02.004991500 
+0100
+++ new/unearth-0.18.3/src/unearth/auth.py      2026-08-14 06:13:27.665497800 
+0200
@@ -6,7 +6,7 @@
 import os
 import shutil
 import subprocess
-from typing import TYPE_CHECKING, Literal, Optional, Tuple, cast
+from typing import TYPE_CHECKING, Literal, Optional, cast
 from urllib.parse import SplitResult, urlparse, urlsplit
 
 from httpx import URL, Auth, BasicAuth
@@ -14,7 +14,8 @@
 from unearth.utils import commonprefix, get_netrc_auth, split_auth_from_url
 
 if TYPE_CHECKING:
-    from typing import Any, Callable, Generator, Iterable
+    from collections.abc import Generator, Iterable
+    from typing import Any, Callable
 
     from httpx import Request, Response
     from requests import Response as RequestsResponse
@@ -22,8 +23,8 @@
 
 KEYRING_DISABLED = False
 
-AuthInfo = Tuple[str, str]
-MaybeAuth = Optional[Tuple[str, Optional[str]]]
+AuthInfo = tuple[str, str]
+MaybeAuth = Optional[tuple[str, Optional[str]]]
 logger = logging.getLogger(__name__)
 
 
@@ -123,7 +124,11 @@
         cmd = [self.keyring, f"--mode={mode}", "get", service_name, username]
         env = dict(os.environ, PYTHONIOENCODING="utf-8")
         res = subprocess.run(
-            cmd, stdin=subprocess.DEVNULL, capture_output=True, env=env
+            cmd,
+            stdin=subprocess.DEVNULL,
+            capture_output=True,
+            env=env,
+            check=False,
         )
         if res.returncode:
             return None
@@ -132,7 +137,7 @@
     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
+            return
 
         cmd = [self.keyring, "set", service_name, username]
         input_ = (password + os.linesep).encode("utf-8")
@@ -149,7 +154,7 @@
         return KeyringModuleProvider()
     except ImportError:
         pass
-    except Exception as exc:
+    except Exception as exc:  # noqa: BLE001
         logger.warning(
             "Importing keyring failed: %s, trying to find a keyring 
executable.",
             exc,
@@ -172,7 +177,7 @@
         return None
     try:
         return keyring.get_auth_info(url, username)
-    except Exception as exc:
+    except Exception as exc:  # noqa: BLE001
         logger.warning(
             "Keyring is skipped due to an exception: %s",
             str(exc),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/collector.py 
new/unearth-0.18.3/src/unearth/collector.py
--- old/unearth-0.18.2/src/unearth/collector.py 2025-12-23 07:40:02.004991500 
+0100
+++ new/unearth-0.18.3/src/unearth/collector.py 2026-08-14 06:13:27.665497800 
+0200
@@ -6,9 +6,10 @@
 import json
 import logging
 import mimetypes
+from collections.abc import Iterable, Mapping
 from datetime import datetime
 from html.parser import HTMLParser
-from typing import Iterable, Mapping, NamedTuple
+from typing import NamedTuple
 from urllib import parse
 
 from unearth.fetchers import Fetcher, Response
@@ -234,12 +235,10 @@
     resp = session.get(
         location.normalized,
         headers={
-            "Accept": ", ".join(
-                [
-                    "application/vnd.pypi.simple.v1+json",
-                    "application/vnd.pypi.simple.v1+html; q=0.1",
-                    "text/html; q=0.01",
-                ]
+            "Accept": (
+                "application/vnd.pypi.simple.v1+json, "
+                "application/vnd.pypi.simple.v1+html; q=0.1, "
+                "text/html; q=0.01"
             ),
             **(headers or {}),
         },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/evaluator.py 
new/unearth-0.18.3/src/unearth/evaluator.py
--- old/unearth-0.18.2/src/unearth/evaluator.py 2025-12-23 07:40:02.004991500 
+0100
+++ new/unearth-0.18.3/src/unearth/evaluator.py 2026-08-14 06:13:27.665497800 
+0200
@@ -184,10 +184,8 @@
                 ) from e
             if not requires_python.contains(py_version, True):
                 raise LinkMismatchError(
-                    "The target python version({}) doesn't match "
-                    "the requires-python specifier {}".format(
-                        py_version, link.requires_python
-                    ),
+                    f"The target python version({py_version}) doesn't match "
+                    f"the requires-python specifier {link.requires_python}",
                 )
 
     def validate_wheel_tag(self, tags: frozenset[Tag]) -> bool:
@@ -296,12 +294,13 @@
     Returns:
         bool: True if the package matches the requirement, False otherwise
     """
-    if requirement.name:
-        if canonicalize_name(package.name) != 
canonicalize_name(requirement.name):
-            logger.debug(
-                "Skipping package %s: name doesn't match %s", package, 
requirement.name
-            )
-            return False
+    if requirement.name and canonicalize_name(package.name) != 
canonicalize_name(
+        requirement.name
+    ):
+        logger.debug(
+            "Skipping package %s: name doesn't match %s", package, 
requirement.name
+        )
+        return False
 
     if package.version and not requirement.specifier.contains(
         package.version, prereleases=allow_prereleases
@@ -338,9 +337,7 @@
         for hash_name, allowed_hashes in hashes.items():
             if hash_name in link_hashes:
                 given_hash = link_hashes[hash_name][0]
-                if given_hash not in allowed_hashes:
-                    return False
-                return True
+                return given_hash in allowed_hashes
 
     hash_name, allowed_hashes = next(iter(hashes.items()))
     given_hash = _get_hash(link, hash_name, session)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/fetchers/__init__.py 
new/unearth-0.18.3/src/unearth/fetchers/__init__.py
--- old/unearth-0.18.2/src/unearth/fetchers/__init__.py 2025-12-23 
07:40:02.004991500 +0100
+++ new/unearth-0.18.3/src/unearth/fetchers/__init__.py 2026-08-14 
06:13:27.665497800 +0200
@@ -1,6 +1,8 @@
 from __future__ import annotations
 
-from typing import ContextManager, Iterable, Iterator, Mapping, Protocol
+from collections.abc import Iterable, Iterator, Mapping
+from contextlib import AbstractContextManager
+from typing import Protocol
 
 from unearth.fetchers.sync import PyPIClient as PyPIClient
 
@@ -45,7 +47,7 @@
 
     def get_stream(
         self, url: str, *, headers: Mapping[str, str] | None = None
-    ) -> ContextManager[Response]: ...
+    ) -> AbstractContextManager[Response]: ...
 
     def __hash__(self) -> int: ...
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/fetchers/legacy.py 
new/unearth-0.18.3/src/unearth/fetchers/legacy.py
--- old/unearth-0.18.2/src/unearth/fetchers/legacy.py   2025-12-23 
07:40:02.005991500 +0100
+++ new/unearth-0.18.3/src/unearth/fetchers/legacy.py   2026-08-14 
06:13:27.665897400 +0200
@@ -7,8 +7,9 @@
 import mimetypes
 import os
 import warnings
+from collections.abc import Iterable, Iterator
 from pathlib import Path
-from typing import Any, Iterable, Iterator, cast
+from typing import Any, cast
 
 import urllib3
 
@@ -69,7 +70,7 @@
                 }
             )
 
-            resp.raw = open(path, "rb")
+            resp.raw = open(path, "rb")  # noqa: SIM115
             resp.close = resp.raw.close  # type: ignore[method-assign]
 
         return resp
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/fetchers/sync.py 
new/unearth-0.18.3/src/unearth/fetchers/sync.py
--- old/unearth-0.18.2/src/unearth/fetchers/sync.py     2025-12-23 
07:40:02.005991500 +0100
+++ new/unearth-0.18.3/src/unearth/fetchers/sync.py     2026-08-14 
06:13:27.665897400 +0200
@@ -14,7 +14,9 @@
 
 if TYPE_CHECKING:
     import ssl
-    from typing import Any, ContextManager, Iterable, Mapping
+    from collections.abc import Iterable, Mapping
+    from contextlib import AbstractContextManager
+    from typing import Any
 
     from httpx._types import CertTypes, TimeoutTypes
 
@@ -117,7 +119,7 @@
 
     def get_stream(
         self, url: str, *, headers: Mapping[str, str] | None = None
-    ) -> ContextManager[httpx.Response]:
+    ) -> AbstractContextManager[httpx.Response]:
         return self.stream("GET", url, headers=headers)
 
     def iter_secure_origins(self) -> Iterable[tuple[str, str, str]]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/finder.py 
new/unearth-0.18.3/src/unearth/finder.py
--- old/unearth-0.18.2/src/unearth/finder.py    2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/finder.py    2026-08-14 06:13:27.665897400 
+0200
@@ -9,9 +9,10 @@
 import pathlib
 import posixpath
 import warnings
+from collections.abc import Generator, Iterable, Sequence
 from datetime import datetime
 from tempfile import TemporaryDirectory
-from typing import TYPE_CHECKING, Any, Generator, Iterable, NamedTuple, 
Sequence
+from typing import TYPE_CHECKING, Any, NamedTuple
 
 import packaging.requirements
 from packaging.utils import BuildTag, canonicalize_name, parse_wheel_filename
@@ -31,7 +32,12 @@
 from unearth.fetchers import Fetcher
 from unearth.fetchers.sync import PyPIClient
 from unearth.link import Link
-from unearth.preparer import noop_download_reporter, noop_unpack_reporter, 
unpack_link
+from unearth.preparer import (
+    download_link,
+    noop_download_reporter,
+    noop_unpack_reporter,
+    unpack_link,
+)
 from unearth.utils import LazySequence
 
 if TYPE_CHECKING:
@@ -460,3 +466,31 @@
                 unpack_reporter=unpack_reporter,
             )
         return file.joinpath(link.subdirectory) if link.subdirectory else file
+
+    def download(
+        self,
+        link: Link,
+        location: str | pathlib.Path,
+        hashes: dict[str, list[str]] | None = None,
+        download_reporter: DownloadReporter = noop_download_reporter,
+    ) -> pathlib.Path:
+        """Download the package at the given link without unpacking it.
+
+        Args:
+            link: The link to download.
+            location: The directory to download the artifact to.
+            hashes: The optional hash dict for validation.
+            download_reporter: The download reporter for progress reporting.
+
+        Returns:
+            The path to the downloaded artifact.
+        """
+        if hashes is None:
+            hashes = link.hash_option
+        return download_link(
+            self.session,
+            link,
+            pathlib.Path(location),
+            hashes,
+            download_reporter,
+        )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/link.py 
new/unearth-0.18.3/src/unearth/link.py
--- old/unearth-0.18.2/src/unearth/link.py      2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/link.py      2026-08-14 06:13:27.665897400 
+0200
@@ -79,7 +79,7 @@
     def __hash__(self) -> int:
         return hash(self.__ident())
 
-    def __eq__(self, __o: object) -> bool:
+    def __eq__(self, __o: object, /) -> bool:
         return isinstance(__o, Link) and self.__ident() == __o.__ident()
 
     @classmethod
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/preparer.py 
new/unearth-0.18.3/src/unearth/preparer.py
--- old/unearth-0.18.2/src/unearth/preparer.py  2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/preparer.py  2026-08-14 06:13:27.665897400 
+0200
@@ -11,8 +11,9 @@
 import stat
 import tarfile
 import zipfile
+from collections.abc import Iterable
 from pathlib import Path
-from typing import TYPE_CHECKING, Iterable, cast
+from typing import TYPE_CHECKING, cast
 
 import httpx
 
@@ -79,12 +80,31 @@
 
 def is_within_directory(directory: str | Path, path: str | Path) -> bool:
     try:
-        Path(path).relative_to(directory)
+        Path(os.path.realpath(path)).relative_to(os.path.realpath(directory))
     except ValueError:
         return False
     return True
 
 
+def safe_makedirs(dest_dir: str | Path, location: str | Path) -> None:
+    """Create directory, then verify the resolved path is still within 
location.
+
+    The post-creation check is necessary because ``is_within_directory`` uses
+    ``os.path.realpath``, which cannot resolve symlinks along a path that does
+    not yet exist.  A malicious archive can first extract a symlink pointing
+    outside ``location`` and then reference a path through that symlink.  By
+    re-checking *after* ``os.makedirs`` has materialised the directory (and
+    any intermediate symlinks have landed on disk), we catch traversals that
+    the pre-creation check misses.
+    """
+    os.makedirs(dest_dir, exist_ok=True)
+    if not is_within_directory(location, dest_dir):
+        raise UnpackError(
+            f"Path traversal detected: {dest_dir!r} resolves outside "
+            f"target directory ({location!r})"
+        )
+
+
 def split_leading_dir(path: str) -> list[str]:
     path = path.lstrip("/").lstrip("\\")
     if "/" in path and (
@@ -187,8 +207,7 @@
 
 def _unzip_archive(filename: Path, location: Path, reporter: UnpackReporter) 
-> None:
     os.makedirs(location, exist_ok=True)
-    zipfp = open(filename, "rb")
-    with zipfile.ZipFile(zipfp, allowZip64=True) as zip:
+    with zipfile.ZipFile(filename, allowZip64=True) as zip:
         leading = has_leading_dir(zip.namelist())
         callback = functools.partial(reporter, filename, 
total=len(zip.infolist()))
         for info in iter_with_callback(zip.infolist(), callback):
@@ -204,11 +223,11 @@
                     f"outside target directory ({location})"
                 )
                 raise UnpackError(message)
-            if fn.endswith("/") or fn.endswith("\\"):
+            if fn.endswith(("/", "\\")):
                 # A directory
-                os.makedirs(fn, exist_ok=True)
+                safe_makedirs(fn, location)
             else:
-                os.makedirs(dir, exist_ok=True)
+                safe_makedirs(dir, location)
                 # Don't use read() to avoid allocating an arbitrarily large
                 # chunk of memory for the file's content
                 with zip.open(name) as fp, open(fn, "wb") as destfp:
@@ -222,7 +241,7 @@
     """Untar the file (with path `filename`) to the destination `location`."""
     os.makedirs(location, exist_ok=True)
     lower_fn = str(filename).lower()
-    if lower_fn.endswith(".gz") or lower_fn.endswith(".tgz"):
+    if lower_fn.endswith((".gz", ".tgz")):
         mode = "r:gz"
     elif lower_fn.endswith(BZ2_EXTENSIONS):
         mode = "r:bz2"
@@ -251,11 +270,24 @@
                 )
                 raise UnpackError(message)
             if member.isdir():
-                os.makedirs(path, exist_ok=True)
+                safe_makedirs(path, location)
             elif member.issym():
+                if os.path.isabs(member.linkname):
+                    link_target = member.linkname
+                else:
+                    link_target = os.path.join(os.path.dirname(path), 
member.linkname)
+                if not is_within_directory(location, link_target):
+                    logger.warning(
+                        "In the tar file %s the member %s -> %s points outside 
%s, skipping",
+                        filename,
+                        member.name,
+                        member.linkname,
+                        location,
+                    )
+                    continue
                 try:
                     tar._extract_member(member, path)
-                except Exception as exc:
+                except Exception as exc:  # noqa: BLE001
                     # Some corrupt tar files seem to produce this
                     # (specifically bad symlinks)
                     logger.warning(
@@ -278,7 +310,7 @@
                         exc,
                     )
                     continue
-                os.makedirs(os.path.dirname(path), exist_ok=True)
+                safe_makedirs(os.path.dirname(path), location)
                 assert fp is not None
                 with open(path, "wb") as destfp:
                     shutil.copyfileobj(fp, destfp)
@@ -323,6 +355,40 @@
         download_reporter(link, 1, 1)
         return location
 
+    artifact = download_link(
+        session,
+        link,
+        download_dir,
+        hashes,
+        download_reporter=download_reporter,
+    )
+    if artifact.is_dir():
+        return artifact
+    if link.is_wheel:
+        if link.is_file:
+            # Use the local file directly
+            return artifact
+        target_file = location / link.filename
+        if target_file != artifact:
+            # For wheels downloaded from remote locations, move it to the 
destination.
+            os.replace(artifact, target_file)
+        return target_file
+
+    unpack_archive(artifact, location, reporter=unpack_reporter)
+    return location
+
+
+def download_link(
+    session: Fetcher,
+    link: Link,
+    download_dir: Path,
+    hashes: dict[str, list[str]] | None = None,
+    download_reporter: DownloadReporter = noop_download_reporter,
+) -> Path:
+    """Download an artifact link without unpacking it."""
+    if link.is_vcs:
+        raise UnpackError("VCS links cannot be downloaded without unpacking")
+
     validator = HashValidator(link, hashes)
     if link.is_file:
         if link.file_path.is_dir():
@@ -334,7 +400,8 @@
         artifact = link.file_path
         validator.validate_path(artifact)
     else:
-        # A remote artfiact link, check the download dir first
+        # A remote artifact link, check the download dir first.
+        download_dir.mkdir(parents=True, exist_ok=True)
         artifact = download_dir / link.filename
         if not _check_downloaded(artifact, hashes):
             with session.get_stream(link.normalized) as resp:
@@ -362,15 +429,4 @@
                             validator.update(chunk)
                             f.write(chunk)
             validator.validate()
-    if link.is_wheel:
-        if link.is_file:
-            # Use the local file directly
-            return artifact
-        target_file = location / link.filename
-        if target_file != artifact:
-            # For wheels downloaded from remote locations, move it to the 
destination.
-            os.replace(artifact, target_file)
-        return target_file
-
-    unpack_archive(artifact, location, reporter=unpack_reporter)
-    return location
+    return artifact
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/utils.py 
new/unearth-0.18.3/src/unearth/utils.py
--- old/unearth-0.18.2/src/unearth/utils.py     2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/utils.py     2026-08-14 06:13:27.665897400 
+0200
@@ -8,10 +8,11 @@
 import os
 import re
 import sys
-import urllib.parse as parse
 import warnings
+from collections.abc import Iterable, Iterator, Sequence
 from pathlib import Path
-from typing import Callable, Iterable, Iterator, Sequence, TypeVar
+from typing import Callable, TypeVar
+from urllib import parse
 from urllib.request import url2pathname
 
 WINDOWS = sys.platform == "win32"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/vcs/base.py 
new/unearth-0.18.3/src/unearth/vcs/base.py
--- old/unearth-0.18.2/src/unearth/vcs/base.py  2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/vcs/base.py  2026-08-14 06:13:27.666519600 
+0200
@@ -5,8 +5,9 @@
 import os
 import shutil
 import subprocess
+from collections.abc import Collection, Sequence
 from pathlib import Path
-from typing import Collection, Sequence, Type, TypeVar, cast
+from typing import TypeVar, cast
 
 from unearth.errors import UnpackError, URLError, VCSBackendError
 from unearth.link import Link
@@ -172,7 +173,6 @@
             rev (str|None): the revision to checkout
             args (list[str | HiddenText]): the arguments to pass to the update 
command
         """
-        pass
 
     @abc.abstractmethod
     def update(
@@ -185,7 +185,6 @@
             rev (str|None): the revision to checkout
             args (list[str | HiddenText]): the arguments to pass to the update 
command
         """
-        pass
 
     @abc.abstractmethod
     def get_remote_url(self, location: Path) -> str:
@@ -195,7 +194,6 @@
     @abc.abstractmethod
     def get_revision(self, location: Path) -> str:
         """Get the commit hash of the repository."""
-        pass
 
     def is_immutable_revision(self, location: Path, link: Link) -> bool:
         """Check if the revision is immutable.
@@ -230,7 +228,7 @@
         return []
 
 
-_V = TypeVar("_V", bound=Type[VersionControl])
+_V = TypeVar("_V", bound=type[VersionControl])
 
 
 class VcsSupport:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/vcs/git.py 
new/unearth-0.18.3/src/unearth/vcs/git.py
--- old/unearth-0.18.2/src/unearth/vcs/git.py   2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/vcs/git.py   2026-08-14 06:13:27.666519600 
+0200
@@ -94,11 +94,8 @@
             # Git fetch would fail with abbreviated commits.
             return False
 
-        if self.has_commit(dest, rev):
-            # Don't fetch if we have the commit locally.
-            return False
-
-        return True
+        # Don't fetch if we have the commit locally.
+        return not self.has_commit(dest, rev)
 
     def has_commit(self, location: Path, rev: str) -> bool:
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/src/unearth/vcs/svn.py 
new/unearth-0.18.3/src/unearth/vcs/svn.py
--- old/unearth-0.18.2/src/unearth/vcs/svn.py   2025-12-23 07:40:02.005991500 
+0100
+++ new/unearth-0.18.3/src/unearth/vcs/svn.py   2026-08-14 06:13:27.666519600 
+0200
@@ -138,7 +138,7 @@
             data = ""
 
         url = None
-        if data.startswith("8") or data.startswith("9") or 
data.startswith("10"):
+        if data.startswith(("8", "9", "10")):
             entries = list(map(str.splitlines, data.split("\n\x0c\n")))
             del entries[0][0]  # get rid of the '8'
             url = entries[0][3]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/tests/test_cli.py 
new/unearth-0.18.3/tests/test_cli.py
--- old/unearth-0.18.2/tests/test_cli.py        1970-01-01 01:00:00.000000000 
+0100
+++ new/unearth-0.18.3/tests/test_cli.py        2026-08-14 06:13:27.668556500 
+0200
@@ -0,0 +1,20 @@
+import json
+
+from unearth.__main__ import cli
+from unearth.evaluator import Package
+from unearth.link import Link
+
+
+def test_download_does_not_unpack(mocker, tmp_path, capsys):
+    link = Link("https://example.org/first-2.0.2.tar.gz";)
+    match = Package("first", "2.0.2", link)
+    finder = mocker.patch("unearth.__main__.PackageFinder").return_value
+    finder.find_matches.return_value = [match]
+    downloaded = tmp_path / link.filename
+    finder.download.return_value = downloaded
+
+    cli(["--no-binary", "--download", str(tmp_path), "first"])
+
+    finder.download.assert_called_once_with(link, str(tmp_path))
+    finder.download_and_unpack.assert_not_called()
+    assert json.loads(capsys.readouterr().out)["local_path"] == 
downloaded.as_posix()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/tests/test_evaluator.py 
new/unearth-0.18.3/tests/test_evaluator.py
--- old/unearth-0.18.2/tests/test_evaluator.py  2025-12-23 07:40:02.007991600 
+0100
+++ new/unearth-0.18.3/tests/test_evaluator.py  2026-08-14 06:13:27.668556500 
+0200
@@ -145,13 +145,17 @@
     "url,match",
     [
         (
-            "https://test.pypi.org/files/click-8.1.3-py3-none-any.whl";
-            "#sha256=1234567890abcdef",
+            (
+                "https://test.pypi.org/files/click-8.1.3-py3-none-any.whl";
+                "#sha256=1234567890abcdef"
+            ),
             True,
         ),
         (
-            "https://test.pypi.org/files/click-8.1.3-py3-none-any.whl";
-            "#sha256=fedcba0987654321",
+            (
+                "https://test.pypi.org/files/click-8.1.3-py3-none-any.whl";
+                "#sha256=fedcba0987654321"
+            ),
             True,
         ),
         (
@@ -251,9 +255,9 @@
         ("8.1.3", ">=8.0", None, True),
         ("7.1", ">=8.0", None, False),
         ("8.0.0a0", ">=8.0.0dev0", None, True),
-        ("8.0.0dev0", ">=7", None, False),
+        ("8.0.0dev0", ">=7", False, False),
         ("8.0.0dev0", ">=7", True, True),
-        ("8.0.0a0", "", None, False),
+        ("8.0.0a0", "", False, False),
         ("8.0.0a0", ">=8.0.0dev0", False, False),
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/unearth-0.18.2/tests/test_finder.py 
new/unearth-0.18.3/tests/test_finder.py
--- old/unearth-0.18.2/tests/test_finder.py     2025-12-23 07:40:02.007991600 
+0100
+++ new/unearth-0.18.3/tests/test_finder.py     2026-08-14 06:13:27.668556500 
+0200
@@ -234,6 +234,34 @@
     assert filename == downloaded
 
 
+def test_download_package_file_without_unpacking(pypi_session, fixtures_dir, 
tmp_path):
+    finder = PackageFinder(
+        session=pypi_session,
+        index_urls=[DEFAULT_INDEX_URL],
+        ignore_compatibility=True,
+    )
+    found = finder.find_best_match("first").best.link
+    download_reports = []
+
+    def download_reporter(link, completed, total):
+        download_reports.append((link, completed, total))
+
+    downloaded = finder.download(
+        found,
+        tmp_path / "download",
+        download_reporter=download_reporter,
+    )
+
+    assert downloaded == tmp_path / "download" / found.filename
+    assert (
+        downloaded.read_bytes()
+        == (fixtures_dir / "files" / found.filename).read_bytes()
+    )
+    assert list(downloaded.parent.iterdir()) == [downloaded]
+    _, completed, total = download_reports[-1]
+    assert completed == total == downloaded.stat().st_size
+
+
 def test_exclude_newer_than(pypi_session, content_type):
     finder = PackageFinder(
         session=pypi_session,

Reply via email to