Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package patool for openSUSE:Factory checked 
in at 2026-09-12 21:21:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/patool (Old)
 and      /work/SRC/openSUSE:Factory/.patool.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "patool"

Sat Sep 12 21:21:57 2026 rev:4 rq:1377553 version:4.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/patool/patool.changes    2026-06-17 
16:20:56.513646249 +0200
+++ /work/SRC/openSUSE:Factory/.patool.new.1265/patool.changes  2026-09-12 
21:25:51.804809602 +0200
@@ -1,0 +2,18 @@
+Sat Sep 12 14:12:45 UTC 2026 - Martin Pluskal <[email protected]>
+
+- update to 4.1.0:
+  4.1.0 (released 04.09.2026)
+    * Add more invalid characters for Windows filenames.
+    * Require Python >= 3.12 for native tarfile extraction filters.
+  4.0.7 (released 03.09.2026)
+    * Fix quoting of shell arguments on Windows systems.
+  4.0.6 (released 03.09.2026)
+    * Prefer zpaqfranz for ZPAQ (.zpaq) archives.
+    * Document dependencies, added SECURITY.md.
+    * Fix quoting of shell arguments on Windows systems.
+- require Python >= 3.12 to match upstream.
+- run upstream testsuite in %check (one upstream-broken password
+  test deselected).
+- spec cleanup: drop Group tag, split %license, tighten %files.
+
+-------------------------------------------------------------------

Old:
----
  patool-4.0.5.tar.gz

New:
----
  patool-4.1.0.tar.gz

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

Other differences:
------------------
++++++ patool.spec ++++++
--- /var/tmp/diff_new_pack.4qVuQx/_old  2026-09-12 21:25:52.367833233 +0200
+++ /var/tmp/diff_new_pack.4qVuQx/_new  2026-09-12 21:25:52.368833275 +0200
@@ -19,23 +19,24 @@
 
 
 Name:           patool
-Version:        4.0.5
+Version:        4.1.0
 Release:        0
 Summary:        Portable Command Line Archive File Manager
 License:        GPL-3.0-or-later
-Group:          Productivity/Archiving/Compression
 URL:            https://github.com/wummel/patool
 Source:         
https://github.com/wummel/patool/releases/download/%{version}/patool-%{version}.tar.gz#/%{name}-%{version}.tar.gz
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
-BuildRequires:  python3-base >= 3.11
+BuildRequires:  python3-base >= 3.12
 BuildRequires:  python3-devel
+BuildRequires:  python3-pytest
 BuildRequires:  python3-setuptools
-BuildArch:      noarch
-
+BuildRequires:  unzip
+BuildRequires:  zip
 # upstream no longer provides a bash completion file
 Obsoletes:      %{name}-bash-completion
 Provides:       %{name}-bash-completion
+BuildArch:      noarch
 
 %description
 patool is a portable command line archive file manager. Various archive types
@@ -62,8 +63,17 @@
 %python3_install
 %fdupes %{buildroot}
 
+%check
+# single-flavor spec (python3_* macros): run the suite with the default
+# interpreter; the bare pytest macro would expand over all pythons flavors.
+# TestPyzipPasswordfile is broken upstream (create_zip takes no password,
+# fails with and without zip installed), so deselect it.
+python3 -m pytest tests/ --deselect 
tests/archives/test_pyzipfile.py::TestPyzipPasswordfile::test_py_zipfile
+
 %files
-%doc COPYING doc/*.txt
+%license COPYING
+%doc doc/*.txt
 %{_bindir}/patool
-%{python3_sitelib}/*
+%{python3_sitelib}/patoolib
+%{python3_sitelib}/patool-%{version}*-info
 

++++++ patool-4.0.5.tar.gz -> patool-4.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/Makefile new/patool-4.1.0/Makefile
--- old/patool-4.0.5/Makefile   2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/Makefile   2026-09-04 12:45:16.000000000 +0200
@@ -20,8 +20,6 @@
 AUTHOR:=$(shell grep "MyName:" patoolib/configuration.py | cut -d '"' -f2)
 APPNAME:=$(shell grep "AppName:" patoolib/configuration.py | cut -d '"' -f2)
 
-# exclude packages that are newer than this
-EXCLUDE_NEWER:="7 days"
 # Pytest options:
 # -s: do not capture stdout/stderr (some tests fail otherwise)
 # --full-trace: print full stacktrace on keyboard interrupts
@@ -34,6 +32,8 @@
 TESTOPTS=
 # python files and directories
 PY_FILES_DIRS:=setup.py patoolib tests doc/web/source
+# python files and directories
+SH_FILES_DIRS:=scripts/*.sh
 
 # Release configuration
 ARCHIVE_SOURCE:=$(APPNAME)-$(VERSION).tar.gz
@@ -115,30 +115,28 @@
        $(MAKE) dist release-gh release-pypi release-homepage github-issues
 
 .PHONY: releasecheck
-releasecheck: update-webmeta checkgit checkchangelog lint test typecheck 
checkgitreleasetag ## check that repo is ready for release
+releasecheck: update-webmeta checkchangelog lint test typecheck checkgit ## 
check that repo is ready for release
 
 .PHONY: checkgit
 checkgit: ## check that git changes are all committed on the main branch
-# check that branch is the main branch
+       # check that branch is the main branch
        @if [ "$(shell git rev-parse --abbrev-ref HEAD)" != 
"$(GIT_MAIN_BRANCH)" ]; then \
          echo "ERROR: current git branch is not '$(GIT_MAIN_BRANCH)'"; \
          git rev-parse --abbrev-ref HEAD; \
          false; \
        fi
-# check for uncommitted versions
+       # check for uncommitted versions
        @if [ -n "$(shell git status --porcelain --untracked-files=all)" ]; 
then \
          echo "ERROR: uncommitted git changes"; \
          git status --porcelain --untracked-files=all; \
          false; \
        fi
-
-.PHONY: checkgitreleasetag
-checkgitreleasetag:    ## check release tag for git exists
+       # check release tag for git exists
        @if [ -z "$(shell git tag -l -- $(GITRELEASETAG))" ]; then \
          echo "ERROR: git tag \"$(GITRELEASETAG)\" does not exist, execute 
'git tag -a $(GITRELEASETAG) -m \"$(GITRELEASETAG)\"'"; \
          false; \
        fi
-# check that release tags is pushed to remote
+       # check that release tags is pushed to remote
        @if ! git ls-remote --exit-code --tags origin $(GITRELEASETAG); then \
          echo "ERROR: git tag \"$(GITRELEASETAG)\" does not exist on remote 
repo, execute 'git push --tags'"; \
        fi
@@ -182,9 +180,20 @@
 ############ Linting and syntax checks ############
 
 .PHONY: lint
-lint: ## lint python code
+lint: lint-py lint-md lint-sh ## run various linters
+
+.PHONY: lint-py
+lint-py:       ## lint python files
        ruff check $(PY_FILES_DIRS)
 
+.PHONY: lint-md
+lint-md:       ## lint markdown files
+       rumdl check *.md doc/*.md
+
+.PHONY: lint-sh
+lint-sh:        ## lint shell scripts
+       shellcheck -x $(SH_FILES_DIRS)
+
 .PHONY: audit
 audit: ## run audit checks
        zizmor --pedantic .github/workflows/python-package.yml
@@ -194,29 +203,38 @@
 reformat: ## format the python code
        ruff check --fix $(PY_FILES_DIRS)
        ruff format $(PY_FILES_DIRS)
+       shfmt -w scripts/
+       tombi format pyproject.toml
 
 .PHONY: checkoutdated checkoutdated-py checkoutdated-gh
-checkoutdated: checkoutdated-py checkoutdated-gh
+checkoutdated: checkoutdated-gh checkoutdated-py
 
 checkoutdated-py:      ## Check for outdated package requirements
        puc \
-         --exclude-newer $(EXCLUDE_NEWER) \
-         --exclude-newer-package "python-update-checker=1 minute" \
+         --constraint constraints.txt \
          check pyproject.toml uv.lock
 
-checkoutdated-gh:      ## check for outdated github projects
+checkoutdated-gh: checkratelimit-gh    ## check for outdated github projects
 # github-check-outdated is a local tool which compares a given version with 
the latest available github release version
 # see https://gist.github.com/wummel/ef14989766009effa4e262b01096fc8c for an 
example implementation
        @echo "Check for outdated Github tools"
        github-check-outdated astral-sh uv "$(shell uv --version | cut -f2 -d" 
")"
        github-check-outdated python cpython v$(shell python --version | cut 
-f2 -d" ") '^v3\.14\.[0-9]+$$'
 
+.PHONY: checkratelimit-gh
+checkratelimit-gh: ## check for rate limiting
+       @(
+               set +e
+               github-check-outdated --check astral-sh uv 0 >/dev/null
+               if [ "$$?" == 2 ]; then exit 1; fi
+       )
+
 
 .PHONY: upgradeoutdated
 upgradeoutdated:       upgradeoutdated-gh upgradeoutdated-py
 
 .PHONY: upgradeoutdated-gh
-upgradeoutdated-gh:
+upgradeoutdated-gh:    checkratelimit-gh ## upgrade github project versions
        sed -i -e 's/uv_version_dev = ".*"/uv_version_dev = "$(shell 
github-check-outdated astral-sh uv 0 | cut -f4 -d" ")"/' pyproject.toml
        sed -i -e 's/ version: ".*"/ version: "$(shell github-check-outdated 
astral-sh uv 0 | cut -f4 -d" ")"/' .github/workflows/python-package.yml
        sed -i -e 's/python_version_dev = ".*"/python_version_dev = "$(shell 
github-check-outdated python cpython 0 '^v3\.14\.[0-9]+$$' | cut -f4 -d" " | 
cut -b2-)"/' pyproject.toml
@@ -224,17 +242,13 @@
 
 .PHONY: upgradeoutdated-py
 upgradeoutdated-py:    ## upgrade dependencies in pyproject.toml and uv.lock
-       puc \
-         --exclude-newer $(EXCLUDE_NEWER) \
-         --exclude-newer-package "python-update-checker=1 minute" \
-         update pyproject.toml
-       uv lock \
-         --exclude-newer $(EXCLUDE_NEWER) \
-         --exclude-newer-package "python-update-checker=1 minute" \
-         --upgrade
+       # upgrade pyproject.toml dependencies
+       puc update pyproject.toml
+       # upgrade depencencies in uv lock file
+       uv lock --upgrade
+       # install upgraded package versions in virtual environment
        $(MAKE) init
 
-
 ############ Testing ############
 
 .PHONY: test
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/PKG-INFO new/patool-4.1.0/PKG-INFO
--- old/patool-4.0.5/PKG-INFO   2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/PKG-INFO   2026-09-04 12:45:16.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: patool
-Version: 4.0.5
+Version: 4.1.0
 Summary: portable archive file manager
 Home-page: https://github.com/wummel/patool
 Author: Bastian Kleineidam
@@ -17,7 +17,7 @@
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Operating System :: OS Independent
-Requires-Python: >=3.11
+Requires-Python: >=3.12
 Description-Content-Type: text/markdown
 License-File: COPYING
 Provides-Extra: argcompletion
@@ -35,8 +35,7 @@
 Dynamic: requires-python
 Dynamic: summary
 
-Patool
-=======
+# Patool
 
 Patool is a portable archive file manager for the command line.
 
@@ -56,16 +55,16 @@
 RZIP (.rz), SHN (.shn), TAR (.tar, .cbt), UDF (.udf), XZ (.xz),
 ZIP (.zip, .jar, .cbz), ZOO (.zoo) and ZSTANDARD (.zst) archive formats.
 
-It relies on helper applications to handle those archive formats
-(for example `xz` for XZ (.xz) archives).
-
-The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively
-and do not require helper applications to be installed.
+The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively.
 When using Python >= 3.14 the ZSTANDARD archive format is also supported 
natively.
 
-Examples
----------
-```
+Other formats require archive programs as helper applications to handle those
+formats (for example `xz` for XZ (.xz) archives).
+The installation instructions (see below) list suggested archive programs.
+
+## Examples
+
+```bash
 # Extract several archives with different formats
 patool extract archive.zip otherarchive.rar
 
@@ -94,13 +93,17 @@
 patool repack linux-2.6.33.tar.gz linux-2.6.33.tar.bz2
 ```
 
-Website and installation
--------------------------
-See https://wummel.github.io/patool/ for more info and downloads.
-See 
[doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md) for 
detailed install instructions.
+## Website
+
+See [https://wummel.github.io/patool/] for more info and downloads.
+
+## Installation and archive programs
+
+See [doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md)
+for detailed install instructions and a list of suggested archive programs.
+
+## API
 
-API
-----
 You can use patool functions from other Python applications.
 Log output uses a Python logging handler named "patool" and
 is [configured](https://github.com/wummel/patool/blob/main/patoolib/log.py)
@@ -109,7 +112,7 @@
 Note that extra options or customization
 for specific archive programs are not supported.
 
-```
+```python
 import patoolib
 patoolib.extract_archive("archive.zip", outdir="/tmp")
 patoolib.test_archive("dist.tar.gz", verbosity=1)
@@ -121,16 +124,15 @@
 patoolib.is_archive("package.deb")
 ```
 
-See https://wummel.github.io/patool/ for detailed API documentation.
+See [https://wummel.github.io/patool/] for detailed API documentation.
+
+## Test suite
 
-Test suite
------------
-Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure the code 
quality.
-The tests are run on each code push by a github runner.
+Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure code 
quality.
+The tests are run on each code push on different platforms and python versions.
 
+## Bash completion
 
-Bash completion
-----------------
 Install the argcomplete python package eg. on Debian/Ubuntu with
 `apt-get install python3-argcomplete`,
 then run
@@ -138,7 +140,6 @@
 After that typing `patool`, a `<SPACE>` and then `<TAB>`
 lists available options and commands.
 
+## Development
 
-Development
-------------
 See 
[doc/development.md](https://github.com/wummel/patool/blob/main/doc/development.md).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/README.md new/patool-4.1.0/README.md
--- old/patool-4.0.5/README.md  2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/README.md  2026-09-04 12:45:16.000000000 +0200
@@ -1,5 +1,4 @@
-Patool
-=======
+# Patool
 
 Patool is a portable archive file manager for the command line.
 
@@ -19,16 +18,16 @@
 RZIP (.rz), SHN (.shn), TAR (.tar, .cbt), UDF (.udf), XZ (.xz),
 ZIP (.zip, .jar, .cbz), ZOO (.zoo) and ZSTANDARD (.zst) archive formats.
 
-It relies on helper applications to handle those archive formats
-(for example `xz` for XZ (.xz) archives).
-
-The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively
-and do not require helper applications to be installed.
+The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively.
 When using Python >= 3.14 the ZSTANDARD archive format is also supported 
natively.
 
-Examples
----------
-```
+Other formats require archive programs as helper applications to handle those
+formats (for example `xz` for XZ (.xz) archives).
+The installation instructions (see below) list suggested archive programs.
+
+## Examples
+
+```bash
 # Extract several archives with different formats
 patool extract archive.zip otherarchive.rar
 
@@ -57,13 +56,17 @@
 patool repack linux-2.6.33.tar.gz linux-2.6.33.tar.bz2
 ```
 
-Website and installation
--------------------------
-See https://wummel.github.io/patool/ for more info and downloads.
-See 
[doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md) for 
detailed install instructions.
+## Website
+
+See [https://wummel.github.io/patool/] for more info and downloads.
+
+## Installation and archive programs
+
+See [doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md)
+for detailed install instructions and a list of suggested archive programs.
+
+## API
 
-API
-----
 You can use patool functions from other Python applications.
 Log output uses a Python logging handler named "patool" and
 is [configured](https://github.com/wummel/patool/blob/main/patoolib/log.py)
@@ -72,7 +75,7 @@
 Note that extra options or customization
 for specific archive programs are not supported.
 
-```
+```python
 import patoolib
 patoolib.extract_archive("archive.zip", outdir="/tmp")
 patoolib.test_archive("dist.tar.gz", verbosity=1)
@@ -84,16 +87,15 @@
 patoolib.is_archive("package.deb")
 ```
 
-See https://wummel.github.io/patool/ for detailed API documentation.
+See [https://wummel.github.io/patool/] for detailed API documentation.
+
+## Test suite
 
-Test suite
------------
-Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure the code 
quality.
-The tests are run on each code push by a github runner.
+Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure code 
quality.
+The tests are run on each code push on different platforms and python versions.
 
+## Bash completion
 
-Bash completion
-----------------
 Install the argcomplete python package eg. on Debian/Ubuntu with
 `apt-get install python3-argcomplete`,
 then run
@@ -101,7 +103,6 @@
 After that typing `patool`, a `<SPACE>` and then `<TAB>`
 lists available options and commands.
 
+## Development
 
-Development
-------------
 See 
[doc/development.md](https://github.com/wummel/patool/blob/main/doc/development.md).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/doc/api.md new/patool-4.1.0/doc/api.md
--- old/patool-4.0.5/doc/api.md 2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/doc/api.md 2026-09-04 12:45:16.000000000 +0200
@@ -1,22 +1,23 @@
-Development with the patoolib library
-======================================
+# Development with the patoolib library
 
 The patool functionality can also be used in other Python programs.
 To do this, install the patool program, import the library and
 use one or more of the convenience functions.
 
+```python
 import patoolib
 try:
     patoolib.extract("myarchive.zip", verbose=True)
     print("Success.")
 except patoolib.util.PatoolError as msg:
     print("Error:", msg)
+```
 
 General rules for all convenience functions:
 
 * All convenience functions raise PatoolError on errors.
 
-* If your application is not interactive, you should set ``interactive=False``.
+* If your application is not interactive, you should set `interactive=False`.
   Otherwise the function call could block while it waits for user input.
 
 * Error messages are printed on stderr, informative messages
@@ -35,23 +36,25 @@
 
 The convenience functions are:
 
-* ``def extract_archive(archive, verbosity=0, outdir=None, program=None, 
interactive=True, password=None)``
+* `def extract_archive(archive, verbosity=0, outdir=None,`
+  `program=None, interactive=True, password=None)`
 
   Extracts the given archive filename to the current working directory
   or if specified to the given directory name in outdir.
   Checks that the archive exists and is readable before extracting it.
 
-* ``def list_archive(archive, verbosity=1, program=None, interactive=True, 
password=None)``
+* `def list_archive(archive, verbosity=1, program=None, interactive=True, 
password=None)`
 
   Lists the contents of the given archive filename on stdout.
   Checks that the archive exists and is readable before listing it.
 
-* ``def test_archive(archive, verbosity=0, program=None, interactive=True, 
password=None)``
+* `def test_archive(archive, verbosity=0, program=None, interactive=True, 
password=None)`
 
   Tests the given archive filename.
   Checks that the archive exists and is readable before testing it.
 
-* ``def create_archive(archive, filenames, verbosity=0, program=None, 
interactive=True, password=None)``
+* `def create_archive(archive, filenames, verbosity=0, program=None,`
+  `interactive=True, password=None)`
 
   Creates a new archive. The type of archive is determined
   by the archive filename extension.
@@ -59,20 +62,20 @@
   Also checks that the filename list is not empty and that all files exist
   and are readable.
 
-* ``diff_archives(archive1, archive2, verbosity=0, interactive=True)``
+* `diff_archives(archive1, archive2, verbosity=0, interactive=True)`
 
   This function lists differences in the content of the two archives.
   Both archives are extracted and the contents are compared
   recursively with the diff(1) program.
   Checks that both archives exist and are readable.
 
-* ``def search_archive(pattern, archive, verbosity=0, interactive=True, 
password=None)``
+* `def search_archive(pattern, archive, verbosity=0, interactive=True, 
password=None)`
 
   This function searches the given pattern in the archive file contents
   with grep(1).
   Checks that archive exists and is readable.
 
-* ``repack_archive (archive, archive_new, verbosity=0, interactive=True, 
password=None)``
+* `repack_archive (archive, archive_new, verbosity=0, interactive=True, 
password=None)`
 
   This function extracts the contents of the archive and packs them
   into archive_new.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/doc/changelog.txt 
new/patool-4.1.0/doc/changelog.txt
--- old/patool-4.0.5/doc/changelog.txt  2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/doc/changelog.txt  2026-09-04 12:45:16.000000000 +0200
@@ -1,3 +1,23 @@
+4.1.0 (released 04.09.2026)
+  * [Bugfix] Add more invalid characters for Windows filenames.
+  * [Security] Require Python >= 3.12 in order to use Pythons
+    native extraction filters for tarfiles.
+
+4.0.7 (released 03.09.2026)
+  * [Bugfix] Fix quoting of shell arguments on Windows systems.
+
+4.0.6 (released 03.09.2026)
+  * [Feature] Added zpaqfranz as preferred archive program to handle
+    ZPAQ (.zpaq) archives.
+    Closes: GH bug #195
+  * [Docs] Document dependencies in doc/install.md.
+    Closes: GH bug #192
+  * [Security] Added SECURITY.md documenting the security implications
+    of running patool and how to report vulnerabilities.
+    Closes: GH bug #191
+    Closes: GH bug #194
+  * [Security] Fix quoting of shell arguments on Windows systems 
+
 4.0.5 (released 18.05.2026)
   * [Security] Fix generation of files and directories outside
     the given extraction directory when extracting TAR (.tar) archives
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/doc/development.md 
new/patool-4.1.0/doc/development.md
--- old/patool-4.0.5/doc/development.md 2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/doc/development.md 2026-09-04 12:45:16.000000000 +0200
@@ -1,5 +1,4 @@
-Development
-============
+# Development
 
 Development for patool is done with the following environment:
 
@@ -7,7 +6,8 @@
 
 2. A user with sudo permissions to install required system packages.
 
-MacOS or Windows should also be ok to develop patool, but the scripts below 
have been tested only on Debian Linux.
+MacOS or Windows should also be ok to develop patool, but the scripts
+below have been tested only on Debian Linux.
 
 To start developing, run the following commands:
 
@@ -19,76 +19,82 @@
 
    `scripts/install_dev.sh`
 
-   This script installs required system packages and a lot of archive programs 
for testing
-   with `sudo apt-get install <package>`.
-   It also downloads the [uv tool](https://docs.astral.sh/uv/) to `./bin/uv` 
and configures the PATH variable in `.envrc`
+   This script installs required system packages and a lot of archive
+   programs for testing with `sudo apt-get install <package>`.
+   It also downloads the [uv tool](https://docs.astral.sh/uv/) to
+   `./bin/uv` and configures the PATH variable in `.envrc`
    for `direnv` to find it.
 
 3. Create the Python virtual environment
 
    `make init`
 
-   This command uses `uv` to create a virtual Python environment in the 
directory `.venv/`
+   This command uses `uv` to create a virtual Python environment
+   in the directory `.venv/`
    and installs required Python modules for development.
    Rerun this when changing dependencies in pyproject.toml
 
-3. Run the tests
+4. Run the tests
 
    `make test`
 
-   This runs all tests in parallel. Tests detect necessary features and 
programs to run,
+   This runs all tests in parallel. Tests detect necessary features
+   and programs to run,
    so they do not fail but are skipped when archive programs are missing.
 
-4. Now change the code, eg. to implement a new features.
+5. Now change the code, eg. to implement a new features.
    After changing the code you should run the following commands:
 
    `make lint`
 
-   This catches simple coding and syntax errors. Run this first after changing 
the code.
+   This catches simple coding and syntax errors. Run this first after
+   changing the code.
 
    `make test`
 
-   Run the tests again with the changed code. It is a good idea to add new 
tests when
-   adding new features.
+   Run the tests again with the changed code. It is a good idea to
+   add new tests when adding new features.
 
    `make reformat`
 
    Formats the code according to the configured rules in .ruff.toml.
    Run this before git commit.
 
-5. Other make targets are documented in the Makefile itself.
+6. Other make targets are documented in the Makefile itself.
    Use this command to display all make targets with a short explanation:
 
    `make help`
 
-
-
-After committing and pushing new code to a github repository, the Github 
workflow defined
+After committing and pushing new code to a github repository,
+the Github workflow defined
 in `.github/workflows/python-package.yml` runs.
 The python-package.yml workflow tests different architectures and Python 
versions.
-Tests are run on Windows, MacOS and Linux, tested Python versions are Python 
3.12, 3.13 and 3.14.
+Tests are run on Windows, MacOS and Linux, tested Python versions are
+Python 3.12, 3.13 and 3.14.
 
-A useful tool to test your github workflow on a local system is 
https://github.com/nektos/act,
-though it only supports Linux systems.
+A useful tool to test your github workflow on a local system is
+[https://github.com/nektos/act], though it only supports Linux systems.
 
+## Dependencies
 
-Dependencies
---------------
 Dependencies are specified in `pyproject.toml`.
-All dependencies are pinned to a specific version by using the `==x.y.z` 
version specifier.
+All dependencies are pinned to a specific version by using
+the `==x.y.z` version specifier.
 
 1. Optional dependencies are specified under `[project.optional-dependencies]`.
-2. Development dependencies are specified under `[dependency-groups]` in the 
group `dev`.
+2. Development dependencies are specified under `[dependency-groups]`
+   in the group `dev`.
 3. Python and uv versions are specified unter `[tool.patool]`.
 
-The command `make checkoutdated` checks for new version of python, uv and 
optional and development dependencies.
+The command `make checkoutdated` checks for new version of python,
+uv and optional and development dependencies.
 
-The command `make upgradedoutdated` updates all dependencies in pyproject.toml 
and installs them.
+The command `make upgradedoutdated` updates all dependencies in
+`pyproject.toml` and installs them.
 
+## Webpage
 
-Webpage
---------
 The webpage of patool is written with 
[Sphinx](https://www.sphinx-doc.org/en/master/).
-The index page is in `doc/web/source/index.md`.
+The index page is in `doc/web/source/index.txt`.
 When releasing a new version, the webpage is automatically updated.
 See the `release-homepage` target in `Makefile`.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/doc/install.md 
new/patool-4.1.0/doc/install.md
--- old/patool-4.0.5/doc/install.md     2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/doc/install.md     2026-09-04 12:45:16.000000000 +0200
@@ -1,36 +1,30 @@
-Installation
-============
+# Installation
 
 First, install the required software.
 
-1. Python >= 3.12 from https://www.python.org/
-
+1. Python >= 3.12 from [https://www.python.org/]
 
 Now install the application.
 
 1. Installation with pip
 
-   If you have pip installed: ``pip install patool``
-
-2. Installation from source
-
-   a) Installation as root
+   If you have pip installed, run `pip install patool`.
+   To install pip, run `python -m ensurepip`.
 
-      Run ``sudo python setup.py install`` to install patool.
+2. Installation on Linux
 
-   b) Installation as a normal user
+   Several Linux distributions have packaged patool, for example
+   [Debian](https://packages.debian.org/patool),
+   [Arch](https://aur.archlinux.org/packages/patool) or
+   [Fedora](https://packages.fedoraproject.org/pkgs/patool/patool/).
 
-      Run ``python setup.py install --home $HOME``. Note that you have
-      to adjust your PATH and PYTHONPATH environment variables, e.g. by
-      adding the commands ``export PYTHONPATH=$HOME/lib/python`` and
-      ``export PATH=$PATH:$HOME/bin`` to your shell configuration
-      file.
+   Look at the installation instructions for the patool package of
+   your distribution.
+   But you can also use the pip installation above.
 
-      For more information look at the
-      [Modifying Python's search 
path](http://docs.python.org/inst/search-path.html#SECTION000410000000000000000)
-      documentation.
+3. Installation on Windows
 
-3. Optional: install cygwin file, grep and diff packages on Windows
+   See installation with pip above.
 
    On Windows systems, the archive type is only detectable through file 
extensions.
    To be able to detect archives with missing or non-standard file extensions,
@@ -43,9 +37,29 @@
 
    b) Run `setup-x86_64.exe -q -p file,grep,diff`
 
-   c) Add the `c:\cygwin64\bin` directory to your PATH
+   c) Add `c:\cygwin64\bin` directory to your PATH
+
+4. Installation in MacOS
+
+   See installation with pip above.
+   There is not yet a homebrew formula for patool that I know of.
+
+## Suggested archive programs
+
+Installing the following programs lets patool support a wide variety
+of archive formats.
+
+[7-zip](https://www.7-zip.org/) supports a lot of formats, installing
+it is often sufficient for most users.
+
+[Peazip](https://peazip.github.io/) also supports a lot of
+archive formats.
 
+To handle RAR files install either peazip or the
+[7zip-rar](https://packages.debian.org/trixie/7zip-rar) package.
 
-After installation
-------------------
-Patool is now installed. Have fun!
+For more unusual formats (eg. bzip3) see the ArchivePrograms configuration
+at
+[patoolib/__init__.py](https://github.com/wummel/patool/blob/3255e723f2bb88f8d4ee43d384b4a05131664991/patoolib/__init__.py#L164).
+For each archive format the programs that support extracting,
+listing or creating archive files are listed.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patool.egg-info/PKG-INFO 
new/patool-4.1.0/patool.egg-info/PKG-INFO
--- old/patool-4.0.5/patool.egg-info/PKG-INFO   2026-05-18 10:55:30.000000000 
+0200
+++ new/patool-4.1.0/patool.egg-info/PKG-INFO   2026-09-04 12:45:16.000000000 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: patool
-Version: 4.0.5
+Version: 4.1.0
 Summary: portable archive file manager
 Home-page: https://github.com/wummel/patool
 Author: Bastian Kleineidam
@@ -17,7 +17,7 @@
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
 Classifier: Operating System :: OS Independent
-Requires-Python: >=3.11
+Requires-Python: >=3.12
 Description-Content-Type: text/markdown
 License-File: COPYING
 Provides-Extra: argcompletion
@@ -35,8 +35,7 @@
 Dynamic: requires-python
 Dynamic: summary
 
-Patool
-=======
+# Patool
 
 Patool is a portable archive file manager for the command line.
 
@@ -56,16 +55,16 @@
 RZIP (.rz), SHN (.shn), TAR (.tar, .cbt), UDF (.udf), XZ (.xz),
 ZIP (.zip, .jar, .cbz), ZOO (.zoo) and ZSTANDARD (.zst) archive formats.
 
-It relies on helper applications to handle those archive formats
-(for example `xz` for XZ (.xz) archives).
-
-The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively
-and do not require helper applications to be installed.
+The archive formats TAR, ZIP, BZIP2, LZMA and GZIP are supported natively.
 When using Python >= 3.14 the ZSTANDARD archive format is also supported 
natively.
 
-Examples
----------
-```
+Other formats require archive programs as helper applications to handle those
+formats (for example `xz` for XZ (.xz) archives).
+The installation instructions (see below) list suggested archive programs.
+
+## Examples
+
+```bash
 # Extract several archives with different formats
 patool extract archive.zip otherarchive.rar
 
@@ -94,13 +93,17 @@
 patool repack linux-2.6.33.tar.gz linux-2.6.33.tar.bz2
 ```
 
-Website and installation
--------------------------
-See https://wummel.github.io/patool/ for more info and downloads.
-See 
[doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md) for 
detailed install instructions.
+## Website
+
+See [https://wummel.github.io/patool/] for more info and downloads.
+
+## Installation and archive programs
+
+See [doc/install.md](https://github.com/wummel/patool/blob/main/doc/install.md)
+for detailed install instructions and a list of suggested archive programs.
+
+## API
 
-API
-----
 You can use patool functions from other Python applications.
 Log output uses a Python logging handler named "patool" and
 is [configured](https://github.com/wummel/patool/blob/main/patoolib/log.py)
@@ -109,7 +112,7 @@
 Note that extra options or customization
 for specific archive programs are not supported.
 
-```
+```python
 import patoolib
 patoolib.extract_archive("archive.zip", outdir="/tmp")
 patoolib.test_archive("dist.tar.gz", verbosity=1)
@@ -121,16 +124,15 @@
 patoolib.is_archive("package.deb")
 ```
 
-See https://wummel.github.io/patool/ for detailed API documentation.
+See [https://wummel.github.io/patool/] for detailed API documentation.
+
+## Test suite
 
-Test suite
------------
-Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure the code 
quality.
-The tests are run on each code push by a github runner.
+Patool has [extensive unit 
tests](https://github.com/wummel/patool/tree/master/tests) to ensure code 
quality.
+The tests are run on each code push on different platforms and python versions.
 
+## Bash completion
 
-Bash completion
-----------------
 Install the argcomplete python package eg. on Debian/Ubuntu with
 `apt-get install python3-argcomplete`,
 then run
@@ -138,7 +140,6 @@
 After that typing `patool`, a `<SPACE>` and then `<TAB>`
 lists available options and commands.
 
+## Development
 
-Development
-------------
 See 
[doc/development.md](https://github.com/wummel/patool/blob/main/doc/development.md).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patool.egg-info/SOURCES.txt 
new/patool-4.1.0/patool.egg-info/SOURCES.txt
--- old/patool-4.0.5/patool.egg-info/SOURCES.txt        2026-05-18 
10:55:30.000000000 +0200
+++ new/patool-4.1.0/patool.egg-info/SOURCES.txt        2026-09-04 
12:45:16.000000000 +0200
@@ -97,6 +97,7 @@
 patoolib/programs/zoo.py
 patoolib/programs/zopfli.py
 patoolib/programs/zpaq.py
+patoolib/programs/zpaqfranz.py
 patoolib/programs/zstd.py
 tests/__init__.py
 tests/test_cli.py
@@ -111,6 +112,7 @@
 tests/test_mime.py
 tests/test_repack.py
 tests/test_search.py
+tests/test_shell_quoting.py
 tests/test_test.py
 tests/test_util.py
 tests/test_version.py
@@ -184,6 +186,7 @@
 tests/archives/test_zoo.py
 tests/archives/test_zopfli.py
 tests/archives/test_zpaq.py
+tests/archives/test_zpaqfranz.py
 tests/data/p .7z
 tests/data/p.7z.foo
 tests/data/p.arc
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/__init__.py 
new/patool-4.1.0/patoolib/__init__.py
--- old/patool-4.0.5/patoolib/__init__.py       2026-05-18 10:55:30.000000000 
+0200
+++ new/patool-4.1.0/patoolib/__init__.py       2026-09-04 12:45:16.000000000 
+0200
@@ -19,8 +19,8 @@
 import sys
 
 # check for compatible Python version before importing other packages
-if not hasattr(sys, "version_info") or sys.version_info < (3, 11, 0, "final", 
0):
-    raise SystemExit("This program requires Python 3.11 or later.")
+if not hasattr(sys, "version_info") or sys.version_info < (3, 12, 0, "final", 
0):
+    raise SystemExit("This program requires Python 3.12 or later.")
 import functools
 import inspect
 import os
@@ -388,7 +388,10 @@
         'extract': ('unar',),
     },
     'zpaq': {
-        None: ('zpaq',),
+        None: (
+            'zpaqfranz',
+            'zpaq',
+        ),
     },
     "zstd": {
         None: ("zstd",),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/cli.py 
new/patool-4.1.0/patoolib/cli.py
--- old/patool-4.0.5/patoolib/cli.py    2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/patoolib/cli.py    2026-09-04 12:45:16.000000000 +0200
@@ -325,8 +325,8 @@
     except KeyboardInterrupt:
         log_error("aborted")
         res = 1
-    except Exception:
-        log_internal_error()
+    except Exception as exc:
+        log_internal_error(exc)
         res = 2
     return res
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/configuration.py 
new/patool-4.1.0/patoolib/configuration.py
--- old/patool-4.0.5/patoolib/configuration.py  2026-05-18 10:55:30.000000000 
+0200
+++ new/patool-4.1.0/patoolib/configuration.py  2026-09-04 12:45:16.000000000 
+0200
@@ -2,7 +2,7 @@
 """Define basic configuration data like version or application name."""
 
 AppName: str = "patool"
-Version: str = "4.0.5"
+Version: str = "4.1.0"
 MyName: str = "Bastian Kleineidam"
 MyEmail: str = "[email protected]"
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/fileutil.py 
new/patool-4.1.0/patoolib/fileutil.py
--- old/patool-4.0.5/patoolib/fileutil.py       2026-05-18 10:55:30.000000000 
+0200
+++ new/patool-4.1.0/patoolib/fileutil.py       2026-09-04 12:45:16.000000000 
+0200
@@ -15,7 +15,6 @@
 """File and directory utility functions."""
 
 import os
-import sys
 import shutil
 import stat
 import tempfile
@@ -157,11 +156,6 @@
     return getattr(func, '__name__', repr(func))
 
 
-def rmtree_log_error(func: Callable, path: str, exc: str) -> None:
-    """Error log function for shutil.rmtree()."""
-    log_error(f"Error in {funcname(func)}({path}): {exc[1]}")
-
-
 def rmtree_log_exc(func: Callable, path: str, excinfo) -> None:
     """Error log function for shutil.rmtree()."""
     log_error(f"Error in {funcname(func)}({path}): {excinfo}")
@@ -172,10 +166,7 @@
     Errors will be logged.
     """
     make_user_readable(path)
-    if sys.version_info >= (3, 12, 0, "final", 0):
-        shutil.rmtree(path, onexc=rmtree_log_exc)
-    else:
-        shutil.rmtree(path, onerror=rmtree_log_error)
+    shutil.rmtree(path, onexc=rmtree_log_exc)
 
 
 def make_file_readable(filename: str) -> None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/log.py 
new/patool-4.1.0/patoolib/log.py
--- old/patool-4.0.5/patoolib/log.py    2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/patoolib/log.py    2026-09-04 12:45:16.000000000 +0200
@@ -78,14 +78,14 @@
 EnvKeys = ("LANGUAGE", "LC_ALL", "LC_CTYPE", "LANG")
 
 
-def log_internal_error() -> None:
+def log_internal_error(exception) -> None:
     """Print internal error message."""
     now = time.localtime()
     env = os.linesep.join(
         [f"{key}={os.getenv(key)!r}" for key in EnvKeys if os.getenv(key) is 
not None]
     )
     if logger is not None:
-        logger.exception(
+        logger.error(
             encode_safe(
                 f"""********** Oops, I did it again. *************
 
@@ -107,7 +107,8 @@
 {env}
 ******** {configuration.AppName} internal error, over and out ********
 """
-            )
+            ),
+            exc_info=exception,
         )
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/programs/py_tarfile.py 
new/patool-4.1.0/patoolib/programs/py_tarfile.py
--- old/patool-4.0.5/patoolib/programs/py_tarfile.py    2026-05-18 
10:55:30.000000000 +0200
+++ new/patool-4.1.0/patoolib/programs/py_tarfile.py    2026-09-04 
12:45:16.000000000 +0200
@@ -14,9 +14,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """Archive commands for the tarfile Python module."""
 
-from .. import util, fileutil
-import os
-import sys
+from .. import util
 import tarfile
 
 
@@ -37,34 +35,12 @@
     """Extract a TAR archive with the tarfile Python module."""
     try:
         with tarfile.open(archive) as tfile:
-            if sys.version_info >= (3, 12, 0, "final", 0):
-                tfile.extractall(path=outdir, filter='data')
-            else:
-                safe_extract(tfile, outdir)
+            tfile.extractall(path=outdir, filter='data')
     except Exception as err:
         raise util.PatoolError(f"error extracting {archive}") from err
     return
 
 
-def safe_extract(tfile, path):
-    """Helper function to ensure that TAR members will be extracted inside
-    the given path.
-    If a member will be extracted outside the path an Exception is raised.
-    """
-    safe_members = []
-    bad_members = []
-    for member in tfile.getmembers():
-        member_path = os.path.join(path, member.name)
-        if fileutil.is_within_directory(path, member_path):
-            safe_members.append(member)
-        else:
-            bad_members.append(member)
-    tfile.extractall(path, safe_members)
-    if bad_members:
-        filelist = ", ".join(member.name for member in bad_members)
-        raise Exception(f"Unsafe tarfile entries: {filelist}.")
-
-
 def create_tar(archive, compression, cmd, verbosity, interactive, filenames):
     """Create a TAR archive with the tarfile Python module."""
     mode = get_tar_mode(compression)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/programs/zpaqfranz.py 
new/patool-4.1.0/patoolib/programs/zpaqfranz.py
--- old/patool-4.0.5/patoolib/programs/zpaqfranz.py     1970-01-01 
01:00:00.000000000 +0100
+++ new/patool-4.1.0/patoolib/programs/zpaqfranz.py     2026-09-04 
12:45:16.000000000 +0200
@@ -0,0 +1,40 @@
+# Copyright (C) 2026 Bastian Kleineidam
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""Archive commands for the zpaqfranz program."""
+
+import os
+
+
+def extract_zpaq(archive, compression, cmd, verbosity, interactive, outdir):
+    """Extract a ZPAQ archive."""
+    cmdlist = [cmd, 'x', os.path.abspath(archive)]
+    return (cmdlist, {'cwd': outdir})
+
+
+def list_zpaq(archive, compression, cmd, verbosity, interactive):
+    """List a ZPAQ archive."""
+    return [cmd, 'l', archive]
+
+
+def create_zpaq(archive, compression, cmd, verbosity, interactive, filenames):
+    """Create a ZPAQ archive."""
+    cmdlist = [cmd, 'a', archive]
+    cmdlist.extend(filenames)
+    cmdlist.extend(['-method', '4'])
+    return cmdlist
+
+
+# zpaq has no separate test mode, so use listing instead
+test_zpaq = list_zpaq
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/patoolib/util.py 
new/patool-4.1.0/patoolib/util.py
--- old/patool-4.0.5/patoolib/util.py   2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/patoolib/util.py   2026-09-04 12:45:16.000000000 +0200
@@ -51,10 +51,8 @@
     """Run command without error checking.
     @return: command return code
     """
-    # Note that shell_quote_nt() result is not suitable for copy-paste
-    # (especially on Unix systems), but it looks nicer than shell_quote().
     if verbosity >= 0:
-        info = " ".join(map(shell_quote_nt, cmd))
+        info = " ".join(map(repr, cmd))
         log_info(f"running {info}")
     if run_under_pythonw():
         # prevent opening of additional consoles when running with pythonw.exe
@@ -72,7 +70,7 @@
         kwargs['stderr'] = subprocess.DEVNULL
     if kwargs:
         if verbosity > 0:
-            info = ", ".join(f"{k}={shell_quote(str(v))}" for k, v in 
kwargs.items())
+            info = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
             log_info(f"    with {info}")
         if kwargs.get("shell"):
             # for shell calls the command must be a string
@@ -102,18 +100,24 @@
 
 
 def shell_quote_unix(value: str) -> str:
-    """Quote argument for Unix system."""
+    """Quote argument for Unix system, suitable for filename creation."""
     value = value.replace("'", r"'\''")
     return f"'{value}'"
 
 
 def shell_quote_nt(value: str) -> str:
-    """Quote argument for Windows system. Modeled after distutils
-    _nt_quote_args() function.
-    """
-    if " " in value:
-        return f'"{value}"'
-    return value
+    """Quote argument for Windows systems, suitable for filename creation."""
+    illegal_chars = '"<>|?*\t\n'
+    for c in illegal_chars:
+        if c in value:
+            raise ValueError(f"Cannot quote invalid character {c} in value 
{value!r}")
+    if value.endswith("\\"):
+        raise ValueError(f"Cannot quote invalid ending backslash in value 
{value!r}")
+    quoted_value = value
+    # quote percent with leading percent
+    quoted_value = quoted_value.replace('%', '%%')
+    # finally wrap in double quotes
+    return f'"{quoted_value}"'
 
 
 def p7zip_supports_rar(program: str) -> bool:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/pyproject.toml 
new/patool-4.1.0/pyproject.toml
--- old/patool-4.0.5/pyproject.toml     2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/pyproject.toml     2026-09-04 12:45:16.000000000 +0200
@@ -1,14 +1,10 @@
 [project]
 name = "patool"
-version = "4.0.5"
+version = "4.1.0"
 requires-python = ">=3.12"
 dependencies = []
 # these values are set by setup.py
-dynamic = ["scripts", "authors", "keywords", "classifiers", "urls", "license", 
"readme", "description"]
-
-[build-system]
-requires = ["setuptools-reproducible>=0.1"]
-build-backend = "setuptools_reproducible"
+dynamic = ["authors", "classifiers", "description", "keywords", "license", 
"readme", "scripts", "urls"]
 
 [project.optional-dependencies]
 # optional features
@@ -19,37 +15,46 @@
 # these modules are only needed for development
 [dependency-groups]
 dev = [
-  # for using setuptools.setup() in setup.py
-  "setuptools==82.0.1",
+  # for upgrading the version number
+  "bump2version==1.0.1",
+  # Sphinx plugin for markdown parsing
+  "myst-parser==5.1.0",
+  # test optional functionality
+  "patool[argcompletion]",
   # for testing
-  "pytest==9.0.3",
-  # plugin to run tests in parallel
-  "pytest-xdist[psutil]==3.8.0",
+  "pytest==9.1.1",
   # plugin to run tests in random order
   "pytest-randomly==4.1.0",
+  # plugin to run tests in parallel
+  "pytest-xdist[psutil]==3.8.0",
+  # check for new packages
+  "python-update-checker==0.9.0",
   # for python code linting
-  "ruff==0.15.12",
-  # for python type checking
-  "ty==0.0.35",
-  # for generating a Python .whl file
-  "wheel==0.47.0",
+  "ruff==0.16.5",
+  # Markdown style checking and formatting
+  "rumdl==0.2.62",
+  # for using setuptools.setup() in setup.py
+  "setuptools==84.0.0",
   # for generating the web page
   "sphinx==9.1.0",
-  "myst-parser==5.0.0",
-  # for upgrading the version number
-  "bump2version==1.0.1",
+  # reformat toml files
+  "tombi==1.4.1",
+  # for python type checking
+  "ty==0.0.75",
+  # for generating a Python .whl file
+  "wheel==0.48.0",
   # for auditing github actions
-  "zizmor==1.24.1",
-  # check for new packages
-  "python-update-checker==0.6",
-  # test optional functionality
-  "patool[argcompletion]",
+  "zizmor==1.29.0",
 ]
 
-[tool.patool]
+[build-system]
+requires = ["setuptools-reproducible>=0.1"]
+build-backend = "setuptools_reproducible"
+
+[tool.project]
 # pin versions of Python and uv for development
-python_version_dev = "3.14.5"
-uv_version_dev = "0.11.14"
+python_version_dev = "3.14.7"
+uv_version_dev = "0.12.9"
 
 [tool.ty.environment]
 # patool uses some imports and attributes that are only available on windows
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/setup.py new/patool-4.1.0/setup.py
--- old/patool-4.0.5/setup.py   2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/setup.py   2026-09-04 12:45:16.000000000 +0200
@@ -17,8 +17,8 @@
 
 import sys
 
-if not hasattr(sys, "version_info") or sys.version_info < (3, 11, 0, "final", 
0):
-    raise SystemExit("This program requires Python 3.11 or later.")
+if not hasattr(sys, "version_info") or sys.version_info < (3, 12, 0, "final", 
0):
+    raise SystemExit("This program requires Python 3.12 or later.")
 import os
 
 from setuptools import setup
@@ -55,7 +55,7 @@
     author_email=configdata["AuthorEmail"],
     license=configdata["License"],
     url=configdata["Url"],
-    python_requires=">=3.11",
+    python_requires=">=3.12",
     project_urls={
         "Source": "https://github.com/wummel/patool";,
     },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/tests/archives/test_zpaqfranz.py 
new/patool-4.1.0/tests/archives/test_zpaqfranz.py
--- old/patool-4.0.5/tests/archives/test_zpaqfranz.py   1970-01-01 
01:00:00.000000000 +0100
+++ new/patool-4.1.0/tests/archives/test_zpaqfranz.py   2026-09-04 
12:45:16.000000000 +0200
@@ -0,0 +1,37 @@
+# Copyright (C) 2026 Bastian Kleineidam
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""Test the zpaqfranz program"""
+
+from . import ArchiveTest, Content
+from .. import needs_program
+
+
+class TestZpaqfranz(ArchiveTest):
+    """Test class for the zpaqfranz program"""
+
+    program = 'zpaqfranz'
+
+    @needs_program(program)
+    def test_zpaq(self):
+        """Run archive commands with ZPAQ archive."""
+        self.archive_commands('t.zpaq', check=Content.Multifile)
+
+    @needs_program('file')
+    @needs_program(program)
+    def test_zpaq_file(self):
+        """Run archive commands with renamed ZPAQ archive."""
+        self.archive_extract('t.zpaq.foo', check=Content.Multifile)
+        self.archive_test('t.zpaq.foo')
+        self.archive_list('t.zpaq.foo')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/tests/test_shell_quoting.py 
new/patool-4.1.0/tests/test_shell_quoting.py
--- old/patool-4.0.5/tests/test_shell_quoting.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/patool-4.1.0/tests/test_shell_quoting.py        2026-09-04 
12:45:16.000000000 +0200
@@ -0,0 +1,53 @@
+# Copyright (C) 2026 Bastian Kleineidam
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""Test patool extract command."""
+
+import unittest
+import os
+import shutil
+from patoolib import fileutil, cli
+from . import basedir, datadir, needs_program
+
+
+class ShellQuotingTest(unittest.TestCase):
+    """Test extracting files with special characters."""
+
+    @needs_program('gzip')
+    def test_shell_quoting(self):
+        """Extract files with special characters in the filename."""
+        basename = "test_"
+        for c in "&()^`'!%;$":
+            self._extract(basename + c + ".txt")
+
+    def _extract(self, filename):
+        """Run cli function to extract a gzip archive."""
+        tmpdir_in = fileutil.tmpdir(dir=basedir)
+        tmpdir_out = fileutil.tmpdir(dir=basedir)
+        try:
+            orig_archive = os.path.join(datadir, "t.txt.gz")
+            archive = os.path.join(tmpdir_in, filename + ".gz")
+            shutil.copy(orig_archive, archive)
+            args = [
+                "-vv",
+                "--non-interactive",
+                "extract",
+                "--outdir",
+                tmpdir_out,
+                archive,
+            ]
+            cli.main(args=args)
+        finally:
+            fileutil.rmtree(tmpdir_in)
+            fileutil.rmtree(tmpdir_out)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/patool-4.0.5/tests/test_util.py 
new/patool-4.1.0/tests/test_util.py
--- old/patool-4.0.5/tests/test_util.py 2026-05-18 10:55:30.000000000 +0200
+++ new/patool-4.1.0/tests/test_util.py 2026-09-04 12:45:16.000000000 +0200
@@ -29,13 +29,22 @@
         log.log_error(msg)
         try:
             raise Exception(msg)
-        except Exception:
-            log.log_internal_error()
+        except Exception as exc:
+            log.log_internal_error(exc)
 
-    def test_quote(self):
+    def test_quote_unix(self):
         """Test util.shell_quote_unix()"""
         self.assertEqual(util.shell_quote_unix("a b"), "'a b'")
 
+    def test_quote_nt(self):
+        """Test util.shell_quote_nt()"""
+        self.assertEqual(util.shell_quote_nt("a b"), '"a b"')
+        for c in '&()^!':
+            self.assertEqual(util.shell_quote_nt(f"a{c}{c} b"), f'"a{c}{c} b"')
+        for c in '"<>|?*\t\n\\':
+            self.assertRaises(ValueError, util.shell_quote_nt, f"file{c}")
+        self.assertEqual(util.shell_quote_nt("a%USER%b"), '"a%%USER%%b"')
+
     def test_strlist_with_or(self):
         """Test util.strlist()"""
         alist = []

Reply via email to