Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dtrx for openSUSE:Factory checked in at 2026-09-14 16:20:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dtrx (Old) and /work/SRC/openSUSE:Factory/.dtrx.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dtrx" Mon Sep 14 16:20:24 2026 rev:4 rq:1377583 version:8.7.1 Changes: -------- --- /work/SRC/openSUSE:Factory/dtrx/dtrx.changes 2024-07-17 15:16:06.060154707 +0200 +++ /work/SRC/openSUSE:Factory/.dtrx.new.1265/dtrx.changes 2026-09-14 16:20:30.506844796 +0200 @@ -1,0 +2,19 @@ +Sat Sep 12 14:27:06 UTC 2026 - Martin Pluskal <[email protected]> + +- Update to version 8.7.1: + * Add password options for 7z, zip, rar and arj archives + * Fix 7z extraction handling + * Require Python 3.10+, support 3.12 and 3.13, drop dead + Python 2 compat code + * Fix .tar.lz type mapping + * Magic test for archives with zero-length entry +- Narrow %files glob to explicit module and dist-info entries +- Port build to PEP 517 (%python3_pyproject_wheel/_install, + setuptools >= 75): upstream 8.7 dropped setup.py, %python3_build + no longer works +- Add %check import smoke test +- Strip stray shebang from the imported-only dtrx.py module +- Repair 2008 changelog header rejected by check_dates_in_changes + (blank line after separator plus missing ' - ' before author) + +------------------------------------------------------------------- @@ -84,2 +103 @@ - -* Mon Oct 20 2008 Pascal Bleser <[email protected]> 6.3 +* Mon Oct 20 2008 - Pascal Bleser <[email protected]> 6.3 Old: ---- dtrx-8.5.3.tar.gz New: ---- dtrx-8.7.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dtrx.spec ++++++ --- /var/tmp/diff_new_pack.v5mO9i/_old 2026-09-14 16:20:31.392882096 +0200 +++ /var/tmp/diff_new_pack.v5mO9i/_new 2026-09-14 16:20:31.396882265 +0200 @@ -1,7 +1,7 @@ # # spec file for package dtrx # -# Copyright (c) 2024 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: dtrx -Version: 8.5.3 +Version: 8.7.1 Release: 0 Summary: Intelligent Archive Extraction Tool License: GPL-3.0-only @@ -25,7 +25,9 @@ Source: https://github.com/dtrx-py/dtrx/releases/download/%{version}/dtrx-%{version}.tar.gz BuildRequires: python-rpm-macros BuildRequires: python3-devel -BuildRequires: python3-setuptools +BuildRequires: python3-pip +BuildRequires: python3-setuptools >= 75 +BuildRequires: python3-wheel Requires: bzip2 Requires: cpio Requires: gzip @@ -52,16 +54,23 @@ %prep %autosetup +# dtrx.py is imported, never executed (entry point is %%{_bindir}/dtrx); +# drop its stray shebang (rpmlint non-executable-script). +sed -i '1{/^#!/d}' dtrx/dtrx.py %build -%python3_build +%python3_pyproject_wheel %install -%python3_install +%python3_pyproject_install + +%check +PYTHONPATH=%{buildroot}%{python3_sitelib} python3 -B -c "import dtrx.dtrx" %files %license COPYING %doc README.md %{_bindir}/dtrx -%{python3_sitelib}/* +%{python3_sitelib}/dtrx +%{python3_sitelib}/dtrx-%{version}.dist-info ++++++ dtrx-8.5.3.tar.gz -> dtrx-8.7.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/PKG-INFO new/dtrx-8.7.1/PKG-INFO --- old/dtrx-8.5.3/PKG-INFO 2023-05-11 20:56:02.714164000 +0200 +++ new/dtrx-8.7.1/PKG-INFO 2025-12-01 19:23:07.367742500 +0100 @@ -1,28 +1,28 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: dtrx -Version: 8.5.3 +Version: 8.7.1 Summary: Script to intelligently extract multiple archive types -Download-URL: https://github.com/dtrx-py/dtrx -Author: Brett Smith -Author-email: [email protected] -License: GNU General Public License version 3 or later -Project-URL: homepage, http://www.brettcsmith.org/2007/dtrx/ -Project-URL: code, https://github.com/dtrx-py/dtrx +Author-email: Brett Smith <[email protected]> +Maintainer-email: Brett Smith <[email protected]> +License-Expression: GPL-3.0-or-later +Project-URL: Homepage, http://www.brettcsmith.org/2007/dtrx/ +Project-URL: Repository, https://github.com/dtrx-py/dtrx +Project-URL: Download, https://github.com/dtrx-py/dtrx Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Utilities -Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: COPYING +Requires-Dist: unsupported-python==1.0.0; platform_system == "Windows" +Dynamic: license-file [](https://github.com/dtrx-py/dtrx) [ - [Contributions](#contributions) - [Issues](#issues) + - [Running Tests](#running-tests) - [Releases](#releases) - - [Invoke + Tests](#invoke--tests) - - [Linting](#linting) - - [Docker](#docker) <!-- tocstop --> @@ -72,14 +70,6 @@ 20.04+, where the `dtrx` apt package was removed from the default ppas (likely due to being python2 only). -I attempted to get the tests all working via `tox` , for which I used a -Dockerfile to try to get some kind of environment consistency. You can run the -tests by running (requires Docker installed): - -```bash -./test.sh -``` - ## Development ### Contributions @@ -93,6 +83,19 @@ example, the archive that failed to extract) or reproduction steps so we can address the problem quickly. +### Running Tests + +The tests are most easily run from Docker. Check out the +[`Dockerfile`](Dockerfile) for how that's set up. + +We push a pre-built image to Docker Hub as `dtrx-py/dtrx:latest`, so you can +pull that image and run the tests like so: + +```bash +docker run --rm -it --volume $(pwd):/workspace ghcr.io/dtrx-py/dtrx:latest \ + bash -c "cp -r /workspace ~/scratch && cd ~/scratch && uv run -- tests/compare.py" +``` + ### Releases Releases are tagged in this repo and published to pypi.org. The release process @@ -101,18 +104,16 @@ 1. update the version specifier: ```bash - # update the VERSION value in dtrx/dtrx.py, then: - ❯ git add dtrx/dtrx.py + # update the version appropriately + ❯ uv version X.Y.Z + ❯ git add pyproject.toml uv.lock ❯ git commit # fill in the commit message ``` -2. create an annotated tag for the release. usually good to put a list of new - commits since the previous tag, for example by listing them with: +2. create an annotated tag for the release. ```bash - ❯ git log $(git describe --tags --abbrev=0)..HEAD --oneline - # create the annotated tag - ❯ git tag -a <version number> + ❯ git tag -a {,-m}X.Y.Z ``` be sure to push the tag, `git push --tags`. @@ -121,67 +122,3 @@ PyPi See the [`Makefile`](Makefile) for details on what that rule does. - -### Invoke + Tests - -There's some minimal helper scripts for pyinvoke under [`tasks/`](tasks/). - -To bootstrap, run `pip install -r requirements.txt`, then `inv --list` to see -available tasks: - -```bash -❯ inv --list -Available tasks: - - build-docker build docker image - push-docker push docker image - quick-test run quick tests in docker - rst2man run rst2man in docker - test-nonexistent-file-cmd run test-nonexistent-file-cmd.sh - tox run tox in docker - windows just check that windows install fails. pulls a minimal wine docker image to test -``` - -To run the tests, run `inv tox`. Takes a couple of minutes to go through all the -python versions. - -### Linting - -Linting is provided by [pre-commit](pre-commit.com). To use it, first install -the pre-commit hook: - -```bash -pip install pre-commit -pre-commit install -``` - -pre-commit will run anytime `git commit` runs (disable with `--no-verify`). You -can manually run it with `pre-commit run`. - -### Docker - -The tests in CI (and locally) can be run inside a Docker container, which -provides all the tested python versions. - -This image is defined at [`Dockerfile`](Dockerfile). It's pushed to the GitHub -Container Registry so it can be managed by the `dtrx-py` organization on GitHub- -Docker Hub charges for Organizations. - -There are Invoke tasks for building + pushing the Docker image, which push both -a `:latest` tag as well as a `:2022-09-16` ISO8601 numbered tag. The tag can -then be updated in the GitHub actions runner. - -> Note: there's a bit of complexity around how the image is used, because the -> dtrx tests need to run as a non-root user (there's one test that checks for -> error handling when the output directory is not accessible by the current -> user). To deal with this, there's an entrypoint script that switches user to a -> non-root user, but that still has read/write access to the mounted host volume -> (which is the cwd, intended for local development work). This is required on -> Linux, where it's nice to have the host+container UID+GUID matching, so any -> changes to the mounted host volume have the same permissions set. -> -> In the GitHub actions runner, we need to run inside the same container (to -> have access to the correct python versions for testing), and the github action -> for checkout assumes it can write to somewhat arbitrary locations in the file -> system (basically root access). So we switch to the non-root user _after_ -> checkout. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/README.md new/dtrx-8.7.1/README.md --- old/dtrx-8.5.3/README.md 2023-04-28 22:50:11.000000000 +0200 +++ new/dtrx-8.7.1/README.md 2025-11-03 21:22:47.000000000 +0100 @@ -12,10 +12,8 @@ - [Development](#development) - [Contributions](#contributions) - [Issues](#issues) + - [Running Tests](#running-tests) - [Releases](#releases) - - [Invoke + Tests](#invoke--tests) - - [Linting](#linting) - - [Docker](#docker) <!-- tocstop --> @@ -46,14 +44,6 @@ 20.04+, where the `dtrx` apt package was removed from the default ppas (likely due to being python2 only). -I attempted to get the tests all working via `tox` , for which I used a -Dockerfile to try to get some kind of environment consistency. You can run the -tests by running (requires Docker installed): - -```bash -./test.sh -``` - ## Development ### Contributions @@ -67,6 +57,19 @@ example, the archive that failed to extract) or reproduction steps so we can address the problem quickly. +### Running Tests + +The tests are most easily run from Docker. Check out the +[`Dockerfile`](Dockerfile) for how that's set up. + +We push a pre-built image to Docker Hub as `dtrx-py/dtrx:latest`, so you can +pull that image and run the tests like so: + +```bash +docker run --rm -it --volume $(pwd):/workspace ghcr.io/dtrx-py/dtrx:latest \ + bash -c "cp -r /workspace ~/scratch && cd ~/scratch && uv run -- tests/compare.py" +``` + ### Releases Releases are tagged in this repo and published to pypi.org. The release process @@ -75,18 +78,16 @@ 1. update the version specifier: ```bash - # update the VERSION value in dtrx/dtrx.py, then: - ❯ git add dtrx/dtrx.py + # update the version appropriately + ❯ uv version X.Y.Z + ❯ git add pyproject.toml uv.lock ❯ git commit # fill in the commit message ``` -2. create an annotated tag for the release. usually good to put a list of new - commits since the previous tag, for example by listing them with: +2. create an annotated tag for the release. ```bash - ❯ git log $(git describe --tags --abbrev=0)..HEAD --oneline - # create the annotated tag - ❯ git tag -a <version number> + ❯ git tag -a {,-m}X.Y.Z ``` be sure to push the tag, `git push --tags`. @@ -95,67 +96,3 @@ PyPi See the [`Makefile`](Makefile) for details on what that rule does. - -### Invoke + Tests - -There's some minimal helper scripts for pyinvoke under [`tasks/`](tasks/). - -To bootstrap, run `pip install -r requirements.txt`, then `inv --list` to see -available tasks: - -```bash -❯ inv --list -Available tasks: - - build-docker build docker image - push-docker push docker image - quick-test run quick tests in docker - rst2man run rst2man in docker - test-nonexistent-file-cmd run test-nonexistent-file-cmd.sh - tox run tox in docker - windows just check that windows install fails. pulls a minimal wine docker image to test -``` - -To run the tests, run `inv tox`. Takes a couple of minutes to go through all the -python versions. - -### Linting - -Linting is provided by [pre-commit](pre-commit.com). To use it, first install -the pre-commit hook: - -```bash -pip install pre-commit -pre-commit install -``` - -pre-commit will run anytime `git commit` runs (disable with `--no-verify`). You -can manually run it with `pre-commit run`. - -### Docker - -The tests in CI (and locally) can be run inside a Docker container, which -provides all the tested python versions. - -This image is defined at [`Dockerfile`](Dockerfile). It's pushed to the GitHub -Container Registry so it can be managed by the `dtrx-py` organization on GitHub- -Docker Hub charges for Organizations. - -There are Invoke tasks for building + pushing the Docker image, which push both -a `:latest` tag as well as a `:2022-09-16` ISO8601 numbered tag. The tag can -then be updated in the GitHub actions runner. - -> Note: there's a bit of complexity around how the image is used, because the -> dtrx tests need to run as a non-root user (there's one test that checks for -> error handling when the output directory is not accessible by the current -> user). To deal with this, there's an entrypoint script that switches user to a -> non-root user, but that still has read/write access to the mounted host volume -> (which is the cwd, intended for local development work). This is required on -> Linux, where it's nice to have the host+container UID+GUID matching, so any -> changes to the mounted host volume have the same permissions set. -> -> In the GitHub actions runner, we need to run inside the same container (to -> have access to the correct python versions for testing), and the github action -> for checkout assumes it can write to somewhat arbitrary locations in the file -> system (basically root access). So we switch to the non-root user _after_ -> checkout. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/dtrx/dtrx.py new/dtrx-8.7.1/dtrx/dtrx.py --- old/dtrx-8.5.3/dtrx/dtrx.py 2023-05-11 20:55:13.000000000 +0200 +++ new/dtrx-8.7.1/dtrx/dtrx.py 2025-12-01 19:20:55.000000000 +0100 @@ -19,12 +19,11 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, see <http://www.gnu.org/licenses/>. -# Python 2.3 string methods: 'rfind', 'rindex', 'rjust', 'rstrip' - from __future__ import absolute_import, print_function import errno import fcntl +import importlib.metadata import itertools import logging import mimetypes @@ -34,40 +33,25 @@ import shutil import signal import stat -import string import struct +import subprocess import sys import tempfile import termios import textwrap import traceback +import urllib.parse as urlparse from functools import cmp_to_key, total_ordering -# Python 3 compatibility hacks commence -try: - import urlparse -except ImportError: - import urllib.parse as urlparse -try: - import subprocess32 as subprocess -except ImportError: - import subprocess - -if sys.version_info[0] >= 3: - get_input = input - def cmp(a, b): - return (a > b) - (a < b) +def cmp(a, b): + return (a > b) - (a < b) -else: - get_input = raw_input # noqa: F821 try: - set -except NameError: - from sets import Set as set - -VERSION = "8.5.3" + VERSION = importlib.metadata.version("dtrx") +except importlib.metadata.PackageNotFoundError: + VERSION = "DEVELOPMENT" VERSION_BANNER = """dtrx version %s Copyright © 2006-2011 Brett Smith <[email protected]> Copyright © 2008 Peter Kelemen <[email protected]> @@ -83,11 +67,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.""" % (VERSION,) -# Python3.6 optparse has a hard time parsing this, so ascii transform it -if sys.version_info[:2] == (3, 6): - VERSION_BANNER = VERSION_BANNER.encode("ascii", errors="ignore").decode("ascii") - - MATCHING_DIRECTORY = 1 ONE_ENTRY_KNOWN = 2 BOMB = 3 @@ -157,7 +136,10 @@ free_close = None def create(self): - return tempfile.mkdtemp(prefix=self.original_name + ".", dir=".") + dirname = tempfile.mkdtemp(prefix=self.original_name + ".", dir=".") + # We want to directory to be relative to current directory + dirname = os.path.join(".", os.path.relpath(dirname)) + return dirname class NonblockingRead(object): @@ -170,27 +152,9 @@ flags = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) - def python_2_readlines(self): - # XXX: There seems to be a bug in Python 2 where readline() returns - # "IOError: [Errno 11] Resource temporarily unavailable" on a - # non-blocking read from a pipe where the output lacks a newline. - # It doesn't happen in Python 3, so this hack can be deleted once we - # no longer care about Python 2. - out = "" - try: - while True: - # read a single byte at a time until we hit IOError - out += self.iostream.read(1).decode("ascii", "ignore") - except IOError: - pass - return out.splitlines(True) - def readlines(self): - if sys.version_info[0] >= 3: - out = self.iostream.readlines() - return [line.decode("ascii", "ignore") for line in out] - else: - return self.python_2_readlines() + out = self.iostream.readlines() + return [line.decode("ascii", "ignore") for line in out] class ExtractorError(Exception): @@ -222,9 +186,7 @@ # option based on what's supported, since this behavior changed if encoding in ("lrzip", "lrz"): # need to check if this version of lrzip supports the -Q option - output = subprocess.check_output( - "lrzip --help", stderr=subprocess.STDOUT, shell=True - ) + output = subprocess.check_output("lrzip --help", stderr=subprocess.STDOUT, shell=True) if b"-Q" in output: decoder = ["lrzcat", "-Q"] else: @@ -237,6 +199,7 @@ self.filename = os.path.realpath(filename) self.encoding = encoding self.ignore_pw = False + self.password = None self.file_count = 0 self.included_archives = [] self.target = None @@ -262,9 +225,7 @@ try: logger.debug("running command: {}".format(command)) processes.append( - subprocess.Popen( - command, stdin=stdin, stdout=stdout, stderr=subprocess.PIPE - ) + subprocess.Popen(command, stdin=stdin, stdout=stdout, stderr=subprocess.PIPE) ) except OSError as error: if error.errno == errno.ENOENT: @@ -279,10 +240,9 @@ try: return pipe.wait(timeout=1) except subprocess.TimeoutExpired: - logging.debug("timeout hit..") + logging.debug("timeout hit...") self.timeout_check(pipe) - # Verify that we're not trying to extract password-protected - # archives in non-interactive mode + # Verify that we're not waiting for a password in non-interactive mode if self.pw_prompted and self.ignore_pw: pipe.kill() # Whatever extractor we're using probably left the @@ -292,16 +252,14 @@ self.stderr = "" raise ExtractorError( "cannot extract encrypted archive '%s' in non-interactive mode" - % (self.filename) + " without a password" % (self.filename) ) def send_stdout_to_dev_null(self): return True def run_pipes(self, final_stdout=None): - has_output_target = ( - True if final_stdout or self.send_stdout_to_dev_null() else False - ) + has_output_target = True if final_stdout or self.send_stdout_to_dev_null() else False if not self.pipes: return elif final_stdout is None: @@ -342,9 +300,9 @@ self.file_count += len(filenames) path = path[start_index:] for filename in filenames: - if ExtractorBuilder.try_by_mimetype( + if ExtractorBuilder.try_by_mimetype(filename) or ExtractorBuilder.try_by_extension( filename - ) or ExtractorBuilder.try_by_extension(filename): + ): self.included_archives.append(os.path.join(path, filename)) def check_contents(self): @@ -400,12 +358,8 @@ def check_success(self, got_files): error_index, error_code = self.first_bad_exit_code() - logger.debug( - "success results: %s %s %s" % (got_files, error_index, self.exit_codes) - ) - if self.is_fatal_error(error_code) or ( - (not got_files) and (error_code is not None) - ): + logger.debug("success results: %s %s %s" % (got_files, error_index, self.exit_codes)) + if self.is_fatal_error(error_code) or ((not got_files) and (error_code is not None)): command = " ".join(self.pipes[error_index][0]) self.pw_prompt = False # Don't silently fail with wrong password raise ExtractorError( @@ -417,10 +371,15 @@ self.pipe(self.extract_pipe) self.run_pipes() - def extract(self, ignore_passwd=False): + def extract(self, ignore_passwd=False, password=None): self.ignore_pw = ignore_passwd + self.password = password try: - self.target = tempfile.mkdtemp(prefix=".dtrx-", dir=".") + dirname = tempfile.mkdtemp(prefix=".dtrx-", dir=".") + # We want to directory to be relative to current directory + dirname = os.path.join(".", os.path.relpath(dirname)) + self.target = dirname + except (OSError, IOError) as error: raise ExtractorError("cannot extract here: %s" % (error.strerror,)) old_path = os.path.realpath(os.curdir) @@ -478,14 +437,13 @@ # compression extensions, even if those files shouldn't actually be # handled this way. So, we call out to the file command to do a quick # check and make sure this actually looks like a compressed file. - if "compress" not in [ - match[0] for match in ExtractorBuilder.try_by_magic(self.filename) - ]: + if "compress" not in [match[0] for match in ExtractorBuilder.try_by_magic(self.filename)]: raise ExtractorError("doesn't look like a compressed file") yield self.basename() - def extract(self, ignore_passwd=False): + def extract(self, ignore_passwd=False, password=None): self.ignore_pw = ignore_passwd + self.password = password self.content_type = ONE_ENTRY_KNOWN self.content_name = self.basename() self.contents = None @@ -564,9 +522,7 @@ encoding = mimetypes.guess_type(data_filename)[1] if not encoding: raise ExtractorError("data.tar file has unrecognized encoding") - self.pipe( - ["ar", "p", self.filename, data_filename], "extracting data.tar from .deb" - ) + self.pipe(["ar", "p", self.filename, data_filename], "extracting data.tar from .deb") self.pipe(self.decoders[encoding], "decoding data.tar") def basename(self): @@ -585,9 +541,7 @@ class DebMetadataExtractor(DebExtractor): def prepare(self): - self.pipe( - ["ar", "p", self.filename, "control.tar.gz"], "control.tar.gz extraction" - ) + self.pipe(["ar", "p", self.filename, "control.tar.gz"], "control.tar.gz extraction") self.pipe(["zcat"], "control.tar.gz decompression") @@ -637,9 +591,7 @@ extract_fmt_args = { "OUTPUT_FILE": os.path.splitext(os.path.basename(self.filename))[0], } - formatted_extract_commands = [ - x.format(**extract_fmt_args) for x in self.extract_command - ] + formatted_extract_commands = [x.format(**extract_fmt_args) for x in self.extract_command] self.extract_pipe = formatted_extract_commands + [self.filename] BaseExtractor.extract_archive(self) @@ -651,9 +603,18 @@ class ZipExtractor(NoPipeExtractor): file_type = "Zip file" - extract_command = ["unzip", "-q"] list_command = ["zipinfo", "-1"] + @property + def extract_command(self): + """ + Returns the extraction command and adds a password if given. + """ + cmd = ["unzip", "-q"] + if self.password: + cmd.append("-P %s" % (self.password,)) + return cmd + def is_fatal_error(self, status): return (status or 0) > 1 @@ -702,20 +663,25 @@ class SevenExtractor(NoPipeExtractor): file_type = "7z file" - extract_command = ["7z", "x"] - list_command = ["7z", "l"] + list_command = ["7z", "l", "-ba"] border_re = re.compile("^[- ]+$") + space_re = re.compile(" ") + + @property + def extract_command(self): + """ + Returns the extraction command and adds a password if given. + """ + cmd = ["7z", "x"] + if self.password: + cmd.append("-p%s" % (self.password,)) + return cmd def get_filenames(self): - fn_index = None for line in NoPipeExtractor.get_filenames(self): - if self.border_re.match(line): - if fn_index is not None: - break - else: - fn_index = string.rindex(line, " ") + 1 - elif fn_index is not None: - yield line[fn_index:] + if " " in line: + pos = line.rindex(" ") + 1 + yield line[pos:] self.archive.close() def send_stdout_to_dev_null(self): @@ -747,7 +713,7 @@ if fn_index is not None: break else: - fn_index = string.rindex(line, " ") + 1 + fn_index = line.rindex(" ") + 1 elif fn_index is not None: yield line[fn_index:] self.archive.close() @@ -809,10 +775,19 @@ class RarExtractor(NoPipeExtractor): file_type = "RAR archive" - extract_command = ["unrar", "x"] list_command = ["unrar", "v"] border_re = re.compile("^-+$") + @property + def extract_command(self): + """ + Returns the extraction command and adds a password if given. + """ + cmd = ["unrar", "x"] + if self.password: + cmd.append("-p%s" % (self.password,)) + return cmd + def get_filenames(self): inside = False isfile = True @@ -843,9 +818,18 @@ class UnarchiverExtractor(NoPipeExtractor): file_type = "RAR archive" - extract_command = ["unar", "-D"] list_command = ["lsar"] + @property + def extract_command(self): + """ + Returns the extraction command and adds a password if given. + """ + cmd = ["unar", "-D"] + if self.password: + cmd.append("-p %s" % (self.password,)) + return cmd + def get_filenames(self): output = NoPipeExtractor.get_filenames(self) next(output) @@ -856,10 +840,19 @@ class ArjExtractor(NoPipeExtractor): file_type = "ARJ archive" - extract_command = ["arj", "x", "-y"] list_command = ["arj", "v"] prefix_re = re.compile(r"^\d+\)\s+") + @property + def extract_command(self): + """ + Returns the extraction command and adds a password if given. + """ + cmd = ["arj", "x", "-y"] + if self.password: + cmd.append("-g%s" % (self.password,)) + return cmd + def get_filenames(self): for line in NoPipeExtractor.get_filenames(self): match = self.prefix_re.match(line) @@ -876,19 +869,17 @@ def handle(self): command = "find" - status = subprocess.call( - [ - "find", - self.extractor.target, - "-type", - "d", - "-exec", - "chmod", - "u+rwx", - "{}", - ";", - ] - ) + status = subprocess.call([ + "find", + self.extractor.target, + "-type", + "d", + "-exec", + "chmod", + "u+rwx", + "{}", + ";", + ]) if status == 0: command = "chmod" status = subprocess.call(["chmod", "-R", "u+rwX", self.extractor.target]) @@ -899,9 +890,7 @@ def set_target(self, target, checker): self.target = checker(target).check() if self.target != target: - logger.warning( - "extracting %s to %s" % (self.extractor.filename, self.target) - ) + logger.warning("extracting %s to %s" % (self.extractor.filename, self.target)) # The "where to extract" table, with options and archive types. @@ -932,9 +921,7 @@ if not os.path.isdir(newdir): os.makedirs(newdir) for filename in filenames: - os.rename( - os.path.join(curdir, filename), os.path.join(newdir, filename) - ) + os.rename(os.path.join(curdir, filename), os.path.join(newdir, filename)) os.rmdir(curdir) @@ -960,9 +947,7 @@ ) def organize(self): - source = os.path.join( - self.extractor.target, os.listdir(self.extractor.target)[0] - ) + source = os.path.join(self.extractor.target, os.listdir(self.extractor.target)[0]) if os.path.isdir(source): checker = DirectoryChecker else: @@ -1008,9 +993,7 @@ @total_ordering class BasePolicy(object): try: - size = fcntl.ioctl( - sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0) - ) + size = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack("HHHH", 0, 0, 0, 0)) width = struct.unpack("HHHH", size)[1] except IOError: width = 80 @@ -1036,7 +1019,7 @@ while True: print("\n".join(question)) try: - answer = get_input(self.prompt) + answer = input(self.prompt) except EOFError: return self.answers[""] try: @@ -1104,9 +1087,7 @@ question.append(" Expected: " + extractor.basename()) question.append(" Actual: " + extractor.content_name) choice_vars = (extractor.content_type, extractor.basename()) - self.choices = [ - text % choice_vars[: text.count("%s")] for text in self.choice_template - ] + self.choices = [text % choice_vars[: text.count("%s")] for text in self.choice_template] self.current_policy = self.permanent_policy or self.ask_question(question) def ok_for_match(self): @@ -1140,9 +1121,7 @@ def prep(self, current_filename, target, extractor): archive_count = len(extractor.included_archives) - if (self.permanent_policy is not None) or ( - (archive_count * 10) <= extractor.file_count - ): + if (self.permanent_policy is not None) or ((archive_count * 10) <= extractor.file_count): self.current_policy = self.permanent_policy or RECURSE_NOT_NOW return question = self.wrap( @@ -1162,12 +1141,10 @@ break print( "\n%s\n" - % "\n".join( - [ - os.path.join(target, included_root, filename) - for filename in extractor.included_archives - ] - ) + % "\n".join([ + os.path.join(target, included_root, filename) + for filename in extractor.included_archives + ]) ) if self.current_policy in (RECURSE_ALWAYS, RECURSE_NEVER): self.permanent_policy = self.current_policy @@ -1300,7 +1277,7 @@ ("tar", "gzip", "tar.gz", "tgz"), ("tar", "lzma", "tar.lzma", "tlz"), ("tar", "xz", "tar.xz", "txz"), - ("tar", "lz", "tar.lz"), + ("tar", "lzip", "tar.lz"), ("tar", "compress", "tar.Z", "taz"), ("tar", "lrz", "tar.lrz"), ("tar", "zstd", "tar.zst"), @@ -1376,17 +1353,28 @@ def try_by_magic(self, filename): try: - process = subprocess.Popen( - ["file", "-zL", filename], stdout=subprocess.PIPE + result = subprocess.run( + ["file", "-zL", filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) - status = process.wait() - if status != 0: + if result.returncode != 0: return [] + # if output contains 'ERROR:[', there was an error unzipping the + # first archive entry. re-run without -z. + output = result.stdout.split("\n")[0] + if "ERROR:[" in output: + result = subprocess.run( + ["file", "-L", filename], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + return [] + output = result.stdout.split("\n")[0] + except FileNotFoundError: logger.error("'file' command not found, skipping magic test") return [] - output = process.stdout.readline().decode("ascii") - process.stdout.close() if output.startswith("%s: " % filename): output = output[len(filename) + 2 :] mimes = self.magic_map_matches(output, self.magic_mime_map) @@ -1473,16 +1461,16 @@ print("%s/" % (filename,)) new_filenames = os.listdir(filename) new_filenames = sorted(new_filenames, key=cmp_to_key(reverser)) - filenames.extend( - [pathjoin(filename, new_filename) for new_filename in new_filenames] - ) + filenames.extend([ + pathjoin(filename, new_filename) for new_filename in new_filenames + ]) else: print(filename) def run(self, filename, extractor): self.current_filename = filename error = ( - self.report(extractor.extract, self.options.batch) + self.report(extractor.extract, self.options.batch, self.options.password) or self.report(self.get_handler, extractor) or self.report(self.current_handler.handle) or self.report(self.show_extraction, extractor) @@ -1559,13 +1547,11 @@ # get the lists of built-in extensions and combine them ext_map_base = set(ExtractorBuilder.extension_map.keys()) ext_map = set( - itertools.chain( - *[ - x["extensions"] - for x in ExtractorBuilder.extractor_map.values() - if "extensions" in x - ] - ) + itertools.chain(*[ + x["extensions"] + for x in ExtractorBuilder.extractor_map.values() + if "extensions" in x + ]) ) ext_map = ext_map_base.union(ext_map) @@ -1619,10 +1605,7 @@ "--one-entry", dest="one_entry_default", default=None, - help=( - "specify extraction policy for one-entry " - + "archives: inside/rename/here" - ), + help=("specify extraction policy for one-entry " + "archives: inside/rename/here"), ) parser.add_option( "-n", @@ -1633,6 +1616,13 @@ help="don't ask how to handle special cases", ) parser.add_option( + "-p", + "--password", + dest="password", + default=None, + help="provide a password for password-protected archives", + ) + parser.add_option( "-o", "--overwrite", dest="overwrite", @@ -1737,9 +1727,7 @@ self.current_extractor = extractor # For the abort() method. error = self.action.run(filename, extractor) if error: - errors.append( - (extractor.file_type, extractor.encoding, error, extractor.stderr) - ) + errors.append((extractor.file_type, extractor.encoding, error, extractor.stderr)) if extractor.target is not None: self.clean_destination(extractor.target) else: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/dtrx.egg-info/PKG-INFO new/dtrx-8.7.1/dtrx.egg-info/PKG-INFO --- old/dtrx-8.5.3/dtrx.egg-info/PKG-INFO 2023-05-11 20:56:02.000000000 +0200 +++ new/dtrx-8.7.1/dtrx.egg-info/PKG-INFO 2025-12-01 19:23:07.000000000 +0100 @@ -1,28 +1,28 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: dtrx -Version: 8.5.3 +Version: 8.7.1 Summary: Script to intelligently extract multiple archive types -Download-URL: https://github.com/dtrx-py/dtrx -Author: Brett Smith -Author-email: [email protected] -License: GNU General Public License version 3 or later -Project-URL: homepage, http://www.brettcsmith.org/2007/dtrx/ -Project-URL: code, https://github.com/dtrx-py/dtrx +Author-email: Brett Smith <[email protected]> +Maintainer-email: Brett Smith <[email protected]> +License-Expression: GPL-3.0-or-later +Project-URL: Homepage, http://www.brettcsmith.org/2007/dtrx/ +Project-URL: Repository, https://github.com/dtrx-py/dtrx +Project-URL: Download, https://github.com/dtrx-py/dtrx Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: System Administrators -Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Natural Language :: English Classifier: Operating System :: POSIX Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Utilities -Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 +Requires-Python: >=3.10 Description-Content-Type: text/markdown License-File: COPYING +Requires-Dist: unsupported-python==1.0.0; platform_system == "Windows" +Dynamic: license-file [](https://github.com/dtrx-py/dtrx) [ - [Contributions](#contributions) - [Issues](#issues) + - [Running Tests](#running-tests) - [Releases](#releases) - - [Invoke + Tests](#invoke--tests) - - [Linting](#linting) - - [Docker](#docker) <!-- tocstop --> @@ -72,14 +70,6 @@ 20.04+, where the `dtrx` apt package was removed from the default ppas (likely due to being python2 only). -I attempted to get the tests all working via `tox` , for which I used a -Dockerfile to try to get some kind of environment consistency. You can run the -tests by running (requires Docker installed): - -```bash -./test.sh -``` - ## Development ### Contributions @@ -93,6 +83,19 @@ example, the archive that failed to extract) or reproduction steps so we can address the problem quickly. +### Running Tests + +The tests are most easily run from Docker. Check out the +[`Dockerfile`](Dockerfile) for how that's set up. + +We push a pre-built image to Docker Hub as `dtrx-py/dtrx:latest`, so you can +pull that image and run the tests like so: + +```bash +docker run --rm -it --volume $(pwd):/workspace ghcr.io/dtrx-py/dtrx:latest \ + bash -c "cp -r /workspace ~/scratch && cd ~/scratch && uv run -- tests/compare.py" +``` + ### Releases Releases are tagged in this repo and published to pypi.org. The release process @@ -101,18 +104,16 @@ 1. update the version specifier: ```bash - # update the VERSION value in dtrx/dtrx.py, then: - ❯ git add dtrx/dtrx.py + # update the version appropriately + ❯ uv version X.Y.Z + ❯ git add pyproject.toml uv.lock ❯ git commit # fill in the commit message ``` -2. create an annotated tag for the release. usually good to put a list of new - commits since the previous tag, for example by listing them with: +2. create an annotated tag for the release. ```bash - ❯ git log $(git describe --tags --abbrev=0)..HEAD --oneline - # create the annotated tag - ❯ git tag -a <version number> + ❯ git tag -a {,-m}X.Y.Z ``` be sure to push the tag, `git push --tags`. @@ -121,67 +122,3 @@ PyPi See the [`Makefile`](Makefile) for details on what that rule does. - -### Invoke + Tests - -There's some minimal helper scripts for pyinvoke under [`tasks/`](tasks/). - -To bootstrap, run `pip install -r requirements.txt`, then `inv --list` to see -available tasks: - -```bash -❯ inv --list -Available tasks: - - build-docker build docker image - push-docker push docker image - quick-test run quick tests in docker - rst2man run rst2man in docker - test-nonexistent-file-cmd run test-nonexistent-file-cmd.sh - tox run tox in docker - windows just check that windows install fails. pulls a minimal wine docker image to test -``` - -To run the tests, run `inv tox`. Takes a couple of minutes to go through all the -python versions. - -### Linting - -Linting is provided by [pre-commit](pre-commit.com). To use it, first install -the pre-commit hook: - -```bash -pip install pre-commit -pre-commit install -``` - -pre-commit will run anytime `git commit` runs (disable with `--no-verify`). You -can manually run it with `pre-commit run`. - -### Docker - -The tests in CI (and locally) can be run inside a Docker container, which -provides all the tested python versions. - -This image is defined at [`Dockerfile`](Dockerfile). It's pushed to the GitHub -Container Registry so it can be managed by the `dtrx-py` organization on GitHub- -Docker Hub charges for Organizations. - -There are Invoke tasks for building + pushing the Docker image, which push both -a `:latest` tag as well as a `:2022-09-16` ISO8601 numbered tag. The tag can -then be updated in the GitHub actions runner. - -> Note: there's a bit of complexity around how the image is used, because the -> dtrx tests need to run as a non-root user (there's one test that checks for -> error handling when the output directory is not accessible by the current -> user). To deal with this, there's an entrypoint script that switches user to a -> non-root user, but that still has read/write access to the mounted host volume -> (which is the cwd, intended for local development work). This is required on -> Linux, where it's nice to have the host+container UID+GUID matching, so any -> changes to the mounted host volume have the same permissions set. -> -> In the GitHub actions runner, we need to run inside the same container (to -> have access to the correct python versions for testing), and the github action -> for checkout assumes it can write to somewhat arbitrary locations in the file -> system (basically root access). So we switch to the non-root user _after_ -> checkout. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/dtrx.egg-info/SOURCES.txt new/dtrx-8.7.1/dtrx.egg-info/SOURCES.txt --- old/dtrx-8.5.3/dtrx.egg-info/SOURCES.txt 2023-05-11 20:56:02.000000000 +0200 +++ new/dtrx-8.7.1/dtrx.egg-info/SOURCES.txt 2025-12-01 19:23:07.000000000 +0100 @@ -1,8 +1,6 @@ COPYING README.md pyproject.toml -setup.cfg -setup.py dtrx/__init__.py dtrx/dtrx.py dtrx.egg-info/PKG-INFO diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/dtrx.egg-info/requires.txt new/dtrx-8.7.1/dtrx.egg-info/requires.txt --- old/dtrx-8.5.3/dtrx.egg-info/requires.txt 2023-05-11 20:56:02.000000000 +0200 +++ new/dtrx-8.7.1/dtrx.egg-info/requires.txt 2025-12-01 19:23:07.000000000 +0100 @@ -1,6 +1,3 @@ [:platform_system == "Windows"] unsupported-python==1.0.0 - -[:python_version < "3.3"] -subprocess32 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/pyproject.toml new/dtrx-8.7.1/pyproject.toml --- old/dtrx-8.5.3/pyproject.toml 2022-03-29 04:05:22.000000000 +0200 +++ new/dtrx-8.7.1/pyproject.toml 2025-12-01 19:21:40.000000000 +0100 @@ -1,14 +1,52 @@ -[build-system] -requires = [ - # latest at time of writing - "setuptools >= 44.1.1", - "wheel >= 0.37.0", +[project] +name = "dtrx" +version = "8.7.1" +description = "Script to intelligently extract multiple archive types" +readme = "README.md" +license = "GPL-3.0-or-later" +authors = [{ name = "Brett Smith", email = "[email protected]" }] +maintainers = [{ name = "Brett Smith", email = "[email protected]" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "Operating System :: POSIX", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Utilities", +] +requires-python = ">=3.10" +dependencies = [ + # Intentionally disable windows install, dtrx is not supported on windows + "unsupported-python==1.0.0; platform_system=='Windows'", +] + +[project.urls] +Homepage = "http://www.brettcsmith.org/2007/dtrx/" +Repository = "https://github.com/dtrx-py/dtrx" +Download = "https://github.com/dtrx-py/dtrx" + +[project.scripts] +dtrx = "dtrx.dtrx:main" + +[dependency-groups] +dev = [ + "docutils==0.16", + "pyyaml==5.3.1", + "ruff>=0.14.3", ] -build-backend = 'setuptools.build_meta' -[tool.black] +[tool.ruff] +target-version = "py312" preview = true -target-version = ['py38'] +line-length = 100 + +[build-system] +requires = ["setuptools>=75", "wheel"] +build-backend = "setuptools.build_meta" -[tool.isort] -profile = 'black' +[tool.setuptools.packages.find] +include = ["dtrx*"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/setup.cfg new/dtrx-8.7.1/setup.cfg --- old/dtrx-8.5.3/setup.cfg 2023-05-11 20:56:02.714164000 +0200 +++ new/dtrx-8.7.1/setup.cfg 2025-12-01 19:23:07.367947000 +0100 @@ -1,44 +1,3 @@ -[metadata] -name = dtrx -version = attr: dtrx.dtrx.VERSION -description = Script to intelligently extract multiple archive types -long_description = file: README.md -long_description_content_type = text/markdown -author = Brett Smith -author_email = [email protected] -license = GNU General Public License version 3 or later -classifiers = - Development Status :: 5 - Production/Stable - Environment :: Console - Intended Audience :: End Users/Desktop - Intended Audience :: System Administrators - License :: OSI Approved :: GNU General Public License (GPL) - Natural Language :: English - Operating System :: POSIX - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Programming Language :: Python :: Implementation :: CPython - Topic :: Utilities -download_url = https://github.com/dtrx-py/dtrx -project_urls = - homepage = http://www.brettcsmith.org/2007/dtrx/ - code = https://github.com/dtrx-py/dtrx - -[options] -packages = dtrx -install_requires = - subprocess32;python_version < '3.3' - unsupported-python==1.0.0;platform_system=="Windows" -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.* - -[options.entry_points] -console_scripts = - dtrx = dtrx.dtrx:main - -[bdist_wheel] -universal = 1 - [egg_info] tag_build = tag_date = 0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dtrx-8.5.3/setup.py new/dtrx-8.7.1/setup.py --- old/dtrx-8.5.3/setup.py 2022-11-16 15:51:03.000000000 +0100 +++ new/dtrx-8.7.1/setup.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,7 +0,0 @@ -""" -Thin wrapper for backwards compatibility -""" - -import setuptools - -setuptools.setup()
