Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-lastversion for 
openSUSE:Factory checked in at 2026-10-06 19:16:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-lastversion (Old)
 and      /work/SRC/openSUSE:Factory/.python-lastversion.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-lastversion"

Tue Oct  6 19:16:02 2026 rev:4 rq:1382614 version:3.6.19

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-lastversion/python-lastversion.changes    
2026-08-22 21:37:57.338106982 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-lastversion.new.3698736/python-lastversion.changes
       2026-10-06 19:16:53.641709903 +0200
@@ -1,0 +2,16 @@
+Mon Oct  5 19:07:34 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 3.6.19:
+  * Fixed crash on urllib3 < 1.26 (EL7/EL8 system packages):
+    `Retry() got an unexpected keyword argument
+    'allowed_methods'`
+  * 3.6.18's retry policy now falls back to `method_whitelist` on
+    older urllib3
+  * `.spec` repo resolution now expands spec-defined
+    `%global`/`%define` macros (plus `Name`/`Version`) in
+    `URL`/`Source0`
+    e.g. `URL: https://github.com/%{gitowner}/%{name}` previously
+    queried the literal `%{gitowner}/%{name}`
+  * Transient HTTP failures are retried through the cache adapter
+
+-------------------------------------------------------------------

Old:
----
  lastversion-3.6.17.tar.gz

New:
----
  lastversion-3.6.19.tar.gz

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

Other differences:
------------------
++++++ python-lastversion.spec ++++++
--- /var/tmp/diff_new_pack.lgTPew/_old  2026-10-06 19:16:54.573748905 +0200
+++ /var/tmp/diff_new_pack.lgTPew/_new  2026-10-06 19:16:54.574748947 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-lastversion
-Version:        3.6.17
+Version:        3.6.19
 Release:        0
 Summary:        Find the latest stable release version of an arbitrary project
 License:        BSD-2-Clause

++++++ lastversion-3.6.17.tar.gz -> lastversion-3.6.19.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lastversion-3.6.17/.github/workflows/dockerpublish.yml 
new/lastversion-3.6.19/.github/workflows/dockerpublish.yml
--- old/lastversion-3.6.17/.github/workflows/dockerpublish.yml  2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/.github/workflows/dockerpublish.yml  2026-10-03 
17:06:48.000000000 +0200
@@ -22,7 +22,7 @@
       contents: read
     steps:
       - name: Check out the repo
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Log in to Docker Hub
         uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/.github/workflows/docs.yml 
new/lastversion-3.6.19/.github/workflows/docs.yml
--- old/lastversion-3.6.17/.github/workflows/docs.yml   2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/.github/workflows/docs.yml   2026-10-03 
17:06:48.000000000 +0200
@@ -1,6 +1,11 @@
-name: Deploy docs
+name: Documentation
 
 on:
+  push:
+    branches:
+      - '**'
+    tags-ignore:
+      - '**'
   release:
     types: [created]
 
@@ -11,12 +16,17 @@
     runs-on: ubuntu-latest
 
     steps:
-      - name: Checkout master
-        uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
         with:
           python-version: '3.10'
           cache: 'pip' # caching pip dependencies will speed up subsequent runs
       - run: pip install -e .
       - run: pip install -r docs/requirements.txt
-      - run: mkdocs gh-deploy --force --clean --verbose
+      - name: Build documentation
+        if: github.event_name == 'push'
+        run: mkdocs build --strict
+      - name: Deploy documentation
+        if: github.event_name == 'release'
+        run: mkdocs gh-deploy --strict --force --clean --verbose
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lastversion-3.6.17/.github/workflows/pythonpackage.yml 
new/lastversion-3.6.19/.github/workflows/pythonpackage.yml
--- old/lastversion-3.6.17/.github/workflows/pythonpackage.yml  2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/.github/workflows/pythonpackage.yml  2026-10-03 
17:06:48.000000000 +0200
@@ -22,10 +22,10 @@
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install -e .[tests]
+          pip install -e '.[tests]'
       - name: Test with pytest
         run: |
-          pytest -v -n auto --cov=$REPO_NAME --cov-report=xml 
--junitxml=junit/test-results-3.6.xml
+          pytest -v -n auto --cov="$REPO_NAME" --cov-report=xml 
--junitxml=junit/test-results-3.6.xml
         env:
           # Use custom token if available, otherwise fall back to GITHUB_TOKEN 
(for Dependabot PRs)
           LASTVERSION_GITHUB_API_TOKEN: ${{ 
secrets.LASTVERSION_GITHUB_API_TOKEN || github.token }}
@@ -63,20 +63,20 @@
         python-version: ["3.9", "3.13"]
 
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
     - name: Extract repository name which is also our primary package name
-      run: echo "REPO_NAME=$(basename $GITHUB_REPOSITORY)" >> $GITHUB_ENV
+      run: echo "REPO_NAME=$(basename "$GITHUB_REPOSITORY")" >> "$GITHUB_ENV"
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
         cache: 'pip' # caching pip dependencies will speed up subsequent runs
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install -e .[tests]
+        pip install -e '.[tests]'
     - name: Lint with flake8
       run: |
         # stop the build if there are Python syntax errors or undefined names
@@ -92,7 +92,7 @@
 #        limit-access-to-users: dvershinin
     - name: Test with pytest
       run: |
-        pytest -v -n auto --cov=$REPO_NAME --cov-report=xml 
--junitxml=junit/test-results-${{ matrix.python-version }}.xml
+        pytest -v -n auto --cov="$REPO_NAME" --cov-report=xml 
--junitxml=junit/test-results-${{ matrix.python-version }}.xml
       env:
         # Use custom token if available, otherwise fall back to GITHUB_TOKEN 
(for Dependabot PRs)
         LASTVERSION_GITHUB_API_TOKEN: ${{ secrets.LASTVERSION_GITHUB_API_TOKEN 
|| github.token }}
@@ -124,7 +124,7 @@
     - name: Build updated spec
       run: |
         mkdir -p /github/home/rpmbuild/{SOURCES,SPECS}/
-        mv *.spec /sources/
+        mv ./*.spec /sources/
         build
   build-nginx-module-security-headers-rpm:
     needs: py36
@@ -138,6 +138,6 @@
     - name: Build updated spec
       run: |
         mkdir -p /github/home/rpmbuild/{SOURCES,SPECS}/
-        mv *.spec /sources/
+        mv ./*.spec /sources/
         yum -y install nginx-devel
         build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lastversion-3.6.17/.github/workflows/pythonpublish.yml 
new/lastversion-3.6.19/.github/workflows/pythonpublish.yml
--- old/lastversion-3.6.17/.github/workflows/pythonpublish.yml  2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/.github/workflows/pythonpublish.yml  2026-10-03 
17:06:48.000000000 +0200
@@ -9,9 +9,9 @@
     name: Publish to PyPI
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: '3.x'
         cache: 'pip' # caching pip dependencies will speed up subsequent runs
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/CHANGELOG.md 
new/lastversion-3.6.19/CHANGELOG.md
--- old/lastversion-3.6.17/CHANGELOG.md 2026-08-12 08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/CHANGELOG.md 2026-10-03 17:06:48.000000000 +0200
@@ -1,6 +1,18 @@
 # Changelog
 All notable changes to this project will be documented in this file.
 
+## [3.6.19] - 2026-10-03
+### Fixed
+* Fixed crash on urllib3 < 1.26 (EL7/EL8 system packages): `Retry() got an 
unexpected keyword argument 'allowed_methods'`
+  - 3.6.18's retry policy now falls back to `method_whitelist` on older urllib3
+
+## [3.6.18] - 2026-10-03
+### Fixed
+* `.spec` repo resolution now expands spec-defined `%global`/`%define` macros 
(plus `Name`/`Version`) in `URL`/`Source0`
+  - e.g. `URL: https://github.com/%{gitowner}/%{name}` previously queried the 
literal `%{gitowner}/%{name}`
+### Changed
+* Transient HTTP failures are retried through the cache adapter
+
 ## [3.6.12] - 2026-05-15
 ### Fixed
 * Fixed Docker image publish on release
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/README-ZH-CN.md 
new/lastversion-3.6.19/README-ZH-CN.md
--- old/lastversion-3.6.17/README-ZH-CN.md      2026-08-12 08:44:57.000000000 
+0200
+++ new/lastversion-3.6.19/README-ZH-CN.md      2026-10-03 17:06:48.000000000 
+0200
@@ -8,7 +8,7 @@
 
 ![Using lastversion in 
terminal](https://www.getpagespeed.com/img/lastversion.gif)
 
-[English](README.md) | 简体中文
+[English](https://github.com/dvershinin/lastversion/blob/master/README.md) | 
简体中文
 
 一个轻巧的命令行工具,帮助你查询一个项目/软件的最新版本号及各种相关信息,比如下载链接。
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/mkdocs.yml 
new/lastversion-3.6.19/mkdocs.yml
--- old/lastversion-3.6.17/mkdocs.yml   2026-08-12 08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/mkdocs.yml   2026-10-03 17:06:48.000000000 +0200
@@ -27,6 +27,7 @@
   - src/lastversion
 nav:
   - Overview: index.md
+  - README (中文): README-ZH-CN.md
   - Code Reference:
     - lastversion.py: reference/lastversion.md
     - version.py: reference/version.md
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/src/lastversion/__about__.py 
new/lastversion-3.6.19/src/lastversion/__about__.py
--- old/lastversion-3.6.17/src/lastversion/__about__.py 2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/src/lastversion/__about__.py 2026-10-03 
17:06:48.000000000 +0200
@@ -1,4 +1,4 @@
 """Package metadata"""
 
-__version__ = "3.6.17"
+__version__ = "3.6.19"
 __self__ = "dvershinin/lastversion"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/src/lastversion/lastversion.py 
new/lastversion-3.6.19/src/lastversion/lastversion.py
--- old/lastversion-3.6.17/src/lastversion/lastversion.py       2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/src/lastversion/lastversion.py       2026-10-03 
17:06:48.000000000 +0200
@@ -18,6 +18,7 @@
 import sys
 from os.path import expanduser
 from pathlib import Path
+from typing import Any, List, Optional, Union
 from urllib.parse import urlparse
 
 import requests
@@ -55,9 +56,26 @@
     return spec_urls[0] if spec_urls else None
 
 
-def get_repo_data_from_spec(rpmspec_filename):
+_SPEC_MACRO_DEF_RE = re.compile(r"^%(?:global|define)\s+(\w+)\s+(\S.*?)\s*$")
+_SPEC_MACRO_REF_RE = re.compile(r"%\{\??(\w+)\}|%(\w+)")
+
+
+def expand_spec_macros(value, macros):
+    """Expand %{macro} / %macro references using macros defined in the spec.
+
+    Unknown macros are left as-is. Nested definitions are resolved with a
+    bounded number of passes so self-referencing macros cannot loop forever.
     """
-    Extracts repo data and CLI args from .spec file
+    for _ in range(10):
+        expanded = _SPEC_MACRO_REF_RE.sub(lambda m: macros.get(m.group(1) or 
m.group(2), m.group(0)), value)
+        if expanded == value:
+            break
+        value = expanded
+    return value
+
+
+def get_repo_data_from_spec(rpmspec_filename: str) -> dict:
+    """Extract repo data and CLI args from a spec file.
 
     The project (repo) is specified inside the .spec file
     GitHub repo is resolved via %{upstream_github} + %{name}/%{upstream_name}
@@ -66,10 +84,10 @@
     new version via GitHub, but prepared sources are elsewhere
 
     Args:
-        rpmspec_filename:
+        rpmspec_filename (str): Path to the RPM spec file.
 
     Returns:
-
+        dict: Repository metadata and CLI options parsed from the spec file.
     """
     repo_data = {}
     with open(rpmspec_filename) as f:
@@ -80,7 +98,11 @@
         spec_repo = None
         spec_urls = []
         current_commit = None
+        macros = {}
         for line in f.readlines():
+            macro_def = _SPEC_MACRO_DEF_RE.match(line)
+            if macro_def:
+                macros[macro_def.group(1)] = macro_def.group(2)
             if line.startswith("%global lastversion_repo"):
                 spec_repo = shlex.split(line)[2].strip()
             elif line.startswith("%global upstream_github"):
@@ -93,6 +115,7 @@
                 repo_data["commit_based"] = True
             elif line.startswith("Name:"):
                 name = line.split("Name:")[1].strip()
+                macros["name"] = name
             elif line.startswith("URL:"):
                 # append to spec_urls
                 spec_urls.append(line.split("URL:")[1].strip())
@@ -107,6 +130,7 @@
                 repo_data["module_of"] = True
             elif line.startswith("Version:") and not current_version:
                 current_version = line.split("Version:")[1].strip()
+                macros.setdefault("version", current_version)
             elif line.startswith("%global lastversion_only"):
                 repo_data["only"] = shlex.split(line)[2].strip()
             elif line.startswith("%global lastversion_having_asset"):
@@ -160,7 +184,11 @@
             repo = spec_repo
             log.info("Discovered explicit repo %s from .spec file", repo)
         else:
-            repo = find_preferred_url(spec_urls)
+            # Expand spec-defined macros (e.g. URL: 
https://github.com/%{gitowner}/%{name}),
+            # and prefer URLs that fully resolved over ones still holding 
unknown macros
+            spec_urls = [expand_spec_macros(url, macros) for url in spec_urls]
+            resolved_urls = [url for url in spec_urls if not 
re.search(r"%\{|%[A-Za-z_]", url)]
+            repo = find_preferred_url(resolved_urls or spec_urls)
 
         if not repo:
             log.critical(
@@ -204,16 +232,16 @@
     changelog=False,
     cache_ttl=None,
     skip_release_cache=False,
-):
+) -> Any:
     r"""Find the latest release version for a project.
 
     Args:
         major (str): Only consider versions which are "descendants" of this
-          major version string
+            major version string
         short_urls (bool): Whether we should try to return shorter URLs for
-          release data
+            release data
         assets_filter (Union[str, Pattern]): Regular expression for filtering
-          assets for the latest release
+            assets for the latest release
         only (str): Only consider tags with this text. Useful for repos with 
multiple projects.
                     The argument supports negation and regular expressions. To 
indicate a regex,
                     start it with tilde sign, to negate the expression, start 
it with exclamation
@@ -247,9 +275,8 @@
         <Version('3.0.7')>
 
     Returns:
-        Union[Version, dict]: Newer version object, if found and 
`output_format` is `version`.
-    Returns:
-        str: Single string containing tag, if found and `output_format` is 
`tag`
+        Union[Version, dict, str, None]: Release data in the requested format,
+            or None when no matching release is found.
 
     """
     repo_data = {}
@@ -451,7 +478,7 @@
     return None
 
 
-def clear_cache(repo=None):
+def clear_cache(repo=None) -> int:
     """Clear the HTTP cache for lastversion.
 
     This function is useful for webhook handlers that need to invalidate
@@ -470,7 +497,7 @@
         from lastversion import clear_cache, latest
 
         def handle_github_webhook(payload):
-            repo = payload['repository']['full_name']
+            repo = payload.get("repository", {}).get("full_name")
             clear_cache(repo)
             # Optionally fetch fresh version
             version = latest(repo, output_format='json')
@@ -481,7 +508,7 @@
     return BaseProjectHolder.clear_cache(repo)
 
 
-def has_update(repo, current_version, pre_ok=False, at=None):
+def has_update(repo, current_version, pre_ok=False, at=None) -> Union[Version, 
bool]:
     """Given an existing version for a repo, checks if there is an update.
 
     Args:
@@ -501,7 +528,7 @@
     return False
 
 
-def check_version(value):
+def check_version(value) -> Version:
     """Given a version string, raises argparse.ArgumentTypeError if it does 
not contain any version.
     In lastversion CLI app, this is used as argument parser helper for 
--newer-than (-gt) option.
 
@@ -551,11 +578,16 @@
     return None
 
 
-def build_changelog_bullets(res, repo_arg):
+def build_changelog_bullets(res: dict, repo_arg: str) -> Optional[List[str]]:
     """Build changelog bullets for a release dict using upstream notes and 
OpenAI.
 
+    Args:
+        res (dict): Release metadata containing upstream notes.
+        repo_arg (str): Repository identifier used for the release lookup.
+
     Returns:
-        list[str] or None
+        Optional[List[str]]: Generated changelog bullets, or None when notes
+            are unavailable or generation fails.
     """
     try:
         raw_notes = res.get("body") or res.get("description")
@@ -686,13 +718,13 @@
         f.write("\n".join(out))
 
 
-def update_spec_commit(spec_file, commit_info, repo_data):
+def update_spec_commit(spec_file: str, commit_info: dict, repo_data: dict) -> 
None:
     """Update spec file for commit-based (snapshot) releases.
 
     Args:
-        spec_file: Path to the spec file
-        commit_info: Dict with 'sha', 'short_sha', 'date', 'message'
-        repo_data: Dict with repo data from spec parsing
+        spec_file (str): Path to the spec file.
+        commit_info (dict): Commit metadata with sha, short_sha, date, and 
message fields.
+        repo_data (dict): Repository metadata parsed from the spec file.
 
     Updates:
         - %global commit <sha>
@@ -825,13 +857,13 @@
     #     sys.exit(1)
 
 
-def install_debs(_res, debs, args):
+def install_debs(_res: dict, debs: List[str], args: argparse.Namespace) -> 
None:
     """Install deb packages using apt.
 
     Args:
-        _res: Release dict (unused, kept for API consistency with install_rpms)
-        debs: List of deb package URLs
-        args: CLI arguments
+        _res (dict): Release metadata, unused but kept for API consistency 
with install_rpms.
+        debs (List[str]): Debian package URLs to download and install.
+        args (argparse.Namespace): Parsed CLI arguments.
     """
     try:
         import subprocess
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/lastversion-3.6.17/src/lastversion/repo_holders/base.py 
new/lastversion-3.6.19/src/lastversion/repo_holders/base.py
--- old/lastversion-3.6.17/src/lastversion/repo_holders/base.py 2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/src/lastversion/repo_holders/base.py 2026-10-03 
17:06:48.000000000 +0200
@@ -13,6 +13,7 @@
 from cachecontrol import CacheControlAdapter
 from cachecontrol.caches.file_cache import FileCache
 from packaging.version import InvalidVersion
+from urllib3.util.retry import Retry
 
 from lastversion.__about__ import __version__
 from lastversion.config import get_config
@@ -28,6 +29,38 @@
 
 log = logging.getLogger(__name__)
 
+RETRY_METHODS = frozenset(("GET", "HEAD"))
+
+
+def _make_retry(retries, backoff_factor, retry_cls=Retry):
+    """Build a urllib3 retry policy for idempotent requests across urllib3 
versions.
+
+    urllib3 1.26 renamed ``method_whitelist`` to ``allowed_methods`` and 2.x
+    dropped the old name. EL7/EL8 system packages still ship urllib3 < 1.26,
+    so fall back to the old keyword when the new one is rejected.
+
+    Args:
+        retries (int): Retry budget applied to total, connect, read and status.
+        backoff_factor (float): Exponential backoff factor between attempts.
+        retry_cls (type): Retry class to instantiate (overridable for tests).
+
+    Returns:
+        urllib3.util.retry.Retry: Configured retry policy.
+    """
+    kwargs = {
+        "total": retries,
+        "connect": retries,
+        "read": retries,
+        "status": retries,
+        "backoff_factor": backoff_factor,
+        "status_forcelist": (429, 500, 502, 503, 504),
+        "respect_retry_after_header": True,
+    }
+    try:
+        return retry_cls(allowed_methods=RETRY_METHODS, **kwargs)
+    except TypeError:
+        return retry_cls(method_whitelist=RETRY_METHODS, **kwargs)
+
 
 def _safe_open_write(filename, fmode):
     """Open a file for secure write, mirroring CacheControl's behavior without
@@ -41,7 +74,7 @@
         flags |= os.O_BINARY
     try:
         os.remove(filename)
-    except (IOError, OSError):
+    except OSError:
         pass
     fd = os.open(filename, flags, fmode)
     try:
@@ -115,7 +148,7 @@
         try:
             with open(self._lock_file, "r", encoding="utf-8") as f:
                 return int(f.read().strip())
-        except (IOError, OSError, ValueError):
+        except (OSError, ValueError):
             return None
 
     def _write_lock_file(self):
@@ -159,7 +192,7 @@
             try:
                 with open(old_pid_file, "r", encoding="utf-8") as f:
                     pid = int(f.read().strip())
-            except (IOError, OSError, ValueError):
+            except (OSError, ValueError):
                 pass
 
             if pid is None or not _is_process_alive(pid):
@@ -213,7 +246,7 @@
     def __exit__(self, exc_type, exc, tb):
         try:
             os.remove(self._lock_file)
-        except (IOError, OSError):
+        except OSError:
             pass
         return False
 
@@ -227,7 +260,7 @@
         # Ensure directory exists
         try:
             os.makedirs(os.path.dirname(path), self.dirmode)
-        except (IOError, OSError):
+        except OSError:
             pass
 
         try:
@@ -300,6 +333,8 @@
     # Instance of project holder itself uniquely identifies a project (noname)
     REPO_IS_HOLDER = False
     DEFAULT_TIMEOUT = 30  # default timeout in seconds
+    NETWORK_RETRIES = 5
+    NETWORK_BACKOFF_FACTOR = 0.5
 
     CACHE_DISABLED = False
 
@@ -351,7 +386,7 @@
 
     def __init__(self, name=None, hostname=None):
         super().__init__()
-        self.mount("https://";, requests.adapters.HTTPAdapter(max_retries=5))
+        retries = _make_retry(self.NETWORK_RETRIES, 
self.NETWORK_BACKOFF_FACTOR)
         app_name = __name__.split(".", maxsplit=1)[0]
 
         # Load configuration
@@ -366,7 +401,10 @@
             # Use a lock with a finite timeout to avoid rare hangs on cache 
writes
             lock_cls = InternalTimedDirLock
             self.cache = SafeFileCache(self.cache_dir, lock_class=lock_cls)
-            cache_adapter = CacheControlAdapter(cache=self.cache)
+            # CacheControl replaces the ordinary requests adapter, so the
+            # retry policy must be applied here too. Mounting a retrying
+            # HTTPAdapter first is ineffective once this adapter replaces it.
+            cache_adapter = CacheControlAdapter(cache=self.cache, 
max_retries=retries)
             # noinspection HttpUrlsUsage
             self.mount("http://";, cache_adapter)
             self.mount("https://";, cache_adapter)
@@ -374,6 +412,9 @@
             log.info("Cache is disabled for this holder.")
             # Still need cache_dir for names_cache_filename even if HTTP cache 
is disabled
             self.cache_dir = config.file_cache_path
+            retry_adapter = requests.adapters.HTTPAdapter(max_retries=retries)
+            self.mount("http://";, retry_adapter)
+            self.mount("https://";, retry_adapter)
 
         self.names_cache_filename = f"{self.cache_dir}/repos.json"
 
@@ -466,7 +507,7 @@
                 f"/{quote(repo, safe='')}/",
             ]
 
-            for root, dirs, files in os.walk(cache_dir):
+            for root, _dirs, files in os.walk(cache_dir):
                 for filename in files:
                     filepath = os.path.join(root, filename)
                     # Read the cached URL from the file or use filename 
heuristics
@@ -478,7 +519,7 @@
                         if any(pattern in str(filepath) for pattern in 
repo_patterns):
                             os.remove(filepath)
                             cleared += 1
-                    except (IOError, OSError):
+                    except OSError:
                         pass
 
             # Also clear from names cache
@@ -511,7 +552,7 @@
                 shutil.rmtree(cache_dir)
                 log.info("Cleared all cache from: %s", cache_dir)
                 cleared += 1  # Indicate success
-            except (IOError, OSError) as e:
+            except OSError as e:
                 log.warning("Error clearing cache: %s", e)
 
         return cleared
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/src/lastversion/version.py 
new/lastversion-3.6.19/src/lastversion/version.py
--- old/lastversion-3.6.17/src/lastversion/version.py   2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/src/lastversion/version.py   2026-10-03 
17:06:48.000000000 +0200
@@ -122,18 +122,18 @@
         return part
 
     @staticmethod
-    def join_dashed_number_status(version):
-        """
-        Join status with its number when separated by dash in a version string.
+    def join_dashed_number_status(version: str) -> str:
+        """Join status with its number when separated by a dash.
+
         E.g., 4.27-chaos-preview-3 -> 4.27-chaos-pre3
         Helps devel releases to be correctly identified
         # https://www.python.org/dev/peps/pep-0440/#developmental-releases
 
         Args:
-            version:
+            version (str): Version string to normalize.
 
         Returns:
-            str:
+            str: Version string with dashed status numbers joined.
         """
         for regex, substitution in Version.regex_dashed_substitutions:
             version = regex.sub(substitution, version)
@@ -160,7 +160,7 @@
         # put a delimiter... such string at the beginning typically do not
         # convey stability level, so we are fine to remove them (unlike the
         # ones in the tail)
-        parts_n[0] = re.sub("^[^0-9]+", "", parts_n[0], 1)
+        parts_n[0] = re.sub("^[^0-9]+", "", parts_n[0], count=1)
 
         # Remove empty elements
         parts_n = [item for item in parts_n if item != ""]
@@ -189,7 +189,7 @@
         version = self.filter_relevant_parts(version)
 
         if char_fix_required:
-            version = re.sub("(\\d)([a-z])$", self.fix_letter_post_release, 
version, 1)
+            version = re.sub("(\\d)([a-z])$", self.fix_letter_post_release, 
version, count=1)
         # release-3_0_2 is often seen on Mercurial holders note that the
         # above code removes "release-" already, so we are left with "3_0_2"
         if re.search(r"^(?:\d+_)+(?:\d+)", version):
@@ -272,14 +272,15 @@
             return True
 
     @property
-    def is_prerelease(self):
-        """
-        Version is a prerelease if it contains all the following:
+    def is_prerelease(self) -> bool:
+        """Determine whether this version is a prerelease.
+
+        A version is a prerelease if it contains all the following:
         * 90+ micro component
         * no date in micro component
 
         Returns:
-            bool:
+            bool: True when the version is a prerelease.
         """
         if self.major and self.minor and self.micro >= 90 and 
self.is_not_date(self.micro):
             return True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/tests/test_cli.py 
new/lastversion-3.6.19/tests/test_cli.py
--- old/lastversion-3.6.17/tests/test_cli.py    2026-08-12 08:44:57.000000000 
+0200
+++ new/lastversion-3.6.19/tests/test_cli.py    2026-10-03 17:06:48.000000000 
+0200
@@ -3,7 +3,6 @@
 import os
 import subprocess
 import sys
-import tempfile
 
 from packaging import version
 
@@ -116,33 +115,31 @@
     assert not exit_code  # Check the exit code is correct
 
 
-def test_unzip_osx_bundle_strip(capsys):  # pylint: disable=unused-argument
+def test_unzip_osx_bundle_strip(capsys, monkeypatch, tmp_path):  # pylint: 
disable=unused-argument
     """Test that ZIP files with single top level directory are stripped."""
     with captured_exit_code() as get_exit_code:
-        with tempfile.TemporaryDirectory() as tmp_dir_name:
-            # Set the temp directory as the current working directory
-            os.chdir(tmp_dir_name)
-            main(["--assets", "unzip", 
"lastversion-test-repos/MinimalMIDIPlayer-strip"])
-            # Assert that MinimalMIDIPlayer.app exists and is a directory
-            assert os.path.isdir("Contents")
-            # Assert file MinimalMIDIPlayer.app/Contents/Info.plist exists
-            assert os.path.isfile("Contents/Info.plist")
+        # monkeypatch.chdir restores cwd, so later tests never run in a 
deleted dir
+        monkeypatch.chdir(tmp_path)
+        main(["--assets", "unzip", 
"lastversion-test-repos/MinimalMIDIPlayer-strip"])
+        # Assert that MinimalMIDIPlayer.app exists and is a directory
+        assert os.path.isdir("Contents")
+        # Assert file MinimalMIDIPlayer.app/Contents/Info.plist exists
+        assert os.path.isfile("Contents/Info.plist")
     exit_code = get_exit_code()
 
     assert not exit_code  # Check the exit code is correct
 
 
-def test_unzip_osx_bundle(capsys):  # pylint: disable=unused-argument
+def test_unzip_osx_bundle(capsys, monkeypatch, tmp_path):  # pylint: 
disable=unused-argument
     """Test that OSX bundles are unzipped and .app is not stripped."""
     with captured_exit_code() as get_exit_code:
-        with tempfile.TemporaryDirectory() as tmp_dir_name:
-            # Set the temp directory as the current working directory
-            os.chdir(tmp_dir_name)
-            main(["--assets", "unzip", 
"lastversion-test-repos/MinimalMIDIPlayer"])
-            # Assert that MinimalMIDIPlayer.app exists and is a directory
-            assert os.path.isdir("MinimalMIDIPlayer.app")
-            # Assert file MinimalMIDIPlayer.app/Contents/Info.plist exists
-            assert os.path.isfile("MinimalMIDIPlayer.app/Contents/Info.plist")
+        # monkeypatch.chdir restores cwd, so later tests never run in a 
deleted dir
+        monkeypatch.chdir(tmp_path)
+        main(["--assets", "unzip", "lastversion-test-repos/MinimalMIDIPlayer"])
+        # Assert that MinimalMIDIPlayer.app exists and is a directory
+        assert os.path.isdir("MinimalMIDIPlayer.app")
+        # Assert file MinimalMIDIPlayer.app/Contents/Info.plist exists
+        assert os.path.isfile("MinimalMIDIPlayer.app/Contents/Info.plist")
     exit_code = get_exit_code()
 
     assert not exit_code  # Check the exit code is correct
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/tests/test_github.py 
new/lastversion-3.6.19/tests/test_github.py
--- old/lastversion-3.6.17/tests/test_github.py 2026-08-12 08:44:57.000000000 
+0200
+++ new/lastversion-3.6.19/tests/test_github.py 2026-10-03 17:06:48.000000000 
+0200
@@ -2,7 +2,6 @@
 
 import os
 import re
-from tempfile import TemporaryDirectory
 
 from packaging import version
 
@@ -67,19 +66,18 @@
     assert output == version.parse("5.6")
 
 
-def test_github_extract_wordpress():
+def test_github_extract_wordpress(monkeypatch, tmp_path):
     """
     Test extracting a GitHub WordPress project into the current directory.
     Once extracted, `index.php` should be in the current directory
     """
     repo = "https://github.com/lastversion-test-repos/WordPress";
     with captured_exit_code():
-        # switch to temporary directory
-        with TemporaryDirectory() as tmp_dir:
-            os.chdir(tmp_dir)
-            main(["extract", repo])
-            assert os.path.exists("index.php")
-            assert os.path.exists("wp-config-sample.php")
+        # monkeypatch.chdir restores cwd, so later tests never run in a 
deleted dir
+        monkeypatch.chdir(tmp_path)
+        main(["extract", repo])
+        assert os.path.exists("index.php")
+        assert os.path.exists("wp-config-sample.php")
 
 
 def test_github_search_python():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/tests/test_lastversion.py 
new/lastversion-3.6.19/tests/test_lastversion.py
--- old/lastversion-3.6.17/tests/test_lastversion.py    2026-08-12 
08:44:57.000000000 +0200
+++ new/lastversion-3.6.19/tests/test_lastversion.py    2026-10-03 
17:06:48.000000000 +0200
@@ -611,3 +611,36 @@
         lastversion_mod.update_spec("unused.spec", res, sem="any")
 
     assert exc.value.code == 2
+
+
+def test_spec_url_expands_custom_macros(tmp_path):
+    """URL with spec-defined macros resolves to the real repo (axel-rpm: 
%{gitowner}/%{name})."""
+    spec = tmp_path / "axel.spec"
+    spec.write_text(
+        "%global gitowner axel-download-accelerator\n"
+        "Name:       axel\n"
+        "Version:    2.17.14\n"
+        "URL:        https://github.com/%{gitowner}/%{name}\n";
+        "Source0:    
https://github.com/axel-download-accelerator/axel/releases/download/";
+        "v%{version}/axel-%{version}.tar.gz\n"
+    )
+    repo_data = lastversion_mod.get_repo_data_from_spec(str(spec))
+    assert repo_data["repo"] == 
"https://github.com/axel-download-accelerator/axel";
+
+
+def test_spec_url_with_unknown_macro_falls_back_to_resolved_url(tmp_path):
+    spec = tmp_path / "foo.spec"
+    spec.write_text(
+        "Name: foo\n"
+        "Version: 1.0\n"
+        "URL: https://github.com/%{undefined_owner}/%{name}\n";
+        "Source0: 
https://github.com/owner/foo/archive/v%{version}/foo-%{version}.tar.gz\n";
+    )
+    repo_data = lastversion_mod.get_repo_data_from_spec(str(spec))
+    assert repo_data["repo"] == 
"https://github.com/owner/foo/archive/v1.0/foo-1.0.tar.gz";
+
+
+def test_expand_spec_macros_nested_and_self_referencing():
+    macros = {"a": "%{b}-x", "b": "y", "loop": "%{loop}"}
+    assert lastversion_mod.expand_spec_macros("%{a}/%{?b}/%unknown", macros) 
== "y-x/y/%unknown"
+    assert lastversion_mod.expand_spec_macros("%{loop}", macros) == "%{loop}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/tests/test_lock.py 
new/lastversion-3.6.19/tests/test_lock.py
--- old/lastversion-3.6.17/tests/test_lock.py   2026-08-12 08:44:57.000000000 
+0200
+++ new/lastversion-3.6.19/tests/test_lock.py   2026-10-03 17:06:48.000000000 
+0200
@@ -139,7 +139,7 @@
                 stderr=subprocess.PIPE,
                 timeout=5,
             )
-            assert proc.returncode == 0
+            assert proc.returncode == 0, proc.stderr.decode(errors="replace")
 
             # Lock file should exist but process should be dead
             assert os.path.isfile(lock_file)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lastversion-3.6.17/tests/test_network_retries.py 
new/lastversion-3.6.19/tests/test_network_retries.py
--- old/lastversion-3.6.17/tests/test_network_retries.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/lastversion-3.6.19/tests/test_network_retries.py        2026-10-03 
17:06:48.000000000 +0200
@@ -0,0 +1,58 @@
+"""Regression tests for holder transport retry configuration."""
+
+from cachecontrol import CacheControlAdapter
+
+from lastversion.repo_holders.base import RETRY_METHODS, _make_retry
+from lastversion.repo_holders.test import TestProjectHolder as ProjectHolder
+
+
+def test_cache_adapter_retries_transient_connection_and_server_failures():
+    """Keep retry policy attached after CacheControl replaces the adapter."""
+    holder = ProjectHolder("example")
+
+    adapter = holder.get_adapter("https://example.com/releases";)
+    retries = adapter.max_retries
+
+    assert isinstance(adapter, CacheControlAdapter)
+    assert retries.total == holder.NETWORK_RETRIES
+    assert retries.connect == holder.NETWORK_RETRIES
+    assert retries.read == holder.NETWORK_RETRIES
+    assert retries.status == holder.NETWORK_RETRIES
+    assert retries.backoff_factor == holder.NETWORK_BACKOFF_FACTOR
+    assert set(retries.status_forcelist) == {429, 500, 502, 503, 504}
+    assert retries.respect_retry_after_header is True
+
+
+class _LegacyRetry:
+    """Mimic urllib3 < 1.26 Retry, which only accepts ``method_whitelist``."""
+
+    def __init__(
+        self,
+        total=None,
+        connect=None,
+        read=None,
+        status=None,
+        backoff_factor=0,
+        status_forcelist=None,
+        method_whitelist=None,
+        respect_retry_after_header=True,
+    ):
+        self.total = total
+        self.method_whitelist = method_whitelist
+        self.status_forcelist = status_forcelist
+
+
+def test_make_retry_falls_back_to_method_whitelist_on_old_urllib3():
+    """EL7 ships urllib3 1.25, whose Retry rejects ``allowed_methods``."""
+    retries = _make_retry(3, 0.1, retry_cls=_LegacyRetry)
+
+    assert retries.total == 3
+    assert retries.method_whitelist == RETRY_METHODS
+    assert set(retries.status_forcelist) == {429, 500, 502, 503, 504}
+
+
+def test_make_retry_uses_allowed_methods_on_current_urllib3():
+    """Current urllib3 gets the policy via ``allowed_methods``."""
+    retries = _make_retry(3, 0.1)
+
+    assert retries.allowed_methods == RETRY_METHODS

Reply via email to