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 @@

-[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