diff -pruN 6.20251204.1/.gitlab-ci.yml 6.20251221/.gitlab-ci.yml
--- 6.20251204.1/.gitlab-ci.yml	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/.gitlab-ci.yml	2025-12-21 21:07:09.000000000 +0000
@@ -22,7 +22,16 @@ tests:
 style:
   before_script:
   - apt-get update
-  - apt-get -y install --no-install-recommends pylint
+  - apt-get -y install --no-install-recommends black make pylint
 
   script:
   - pylint dhpython dh_python3 pybuild
+  - make black BLACK_ARGS=--check
+
+typecheck:
+  before_script:
+  - apt-get update
+  - apt-get -y install --no-install-recommends python3-installer mypy make
+
+  script:
+  - make mypy
diff -pruN 6.20251204.1/Makefile 6.20251221/Makefile
--- 6.20251204.1/Makefile	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/Makefile	2025-12-21 21:07:09.000000000 +0000
@@ -53,6 +53,13 @@ nose:
 	#nosetests3 --verbose --with-doctest --with-coverage
 	nose2-3 --verbose --plugin nose2.plugins.doctests --with-doctest
 
+mypy:
+	mypy --strict dh_python3 dhpython/ tests/*.py
+	mypy --strict pybuild
+
+black:
+	black $(BLACK_ARGS) pybuild dh_python3 dhpython/ pydist/*.py tests/*.py
+
 tests: nose
 	make -C tests
 
diff -pruN 6.20251204.1/debian/changelog 6.20251221/debian/changelog
--- 6.20251204.1/debian/changelog	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/debian/changelog	2025-12-21 21:07:09.000000000 +0000
@@ -1,3 +1,26 @@
+dh-python (6.20251221) unstable; urgency=medium
+
+  [ Michael R. Crusoe ]
+  * Enable parallel building of setuptools extensions, both with the distutils
+    backend and the pyproject backend. Parallel building is only enabled
+    if "parallel=N" is present in DEB_BUILD_OPTIONS in accordance with
+    Debian policy § 4.9.1. (Closes: #1093869)
+
+  [ Stefano Rivera ]
+  * Remove tox 3 support.
+  * Add Type annotations and blacken the codebase.
+  * Some related code-base refactoring and cleanup.
+  * Prepend python3-supported-min (>= 3.13.1) | before packages requiring a
+    minimum Python version. (Closes: #1119036)
+  * Keep platform requirements if they apply to one of several supported
+    environments. (Closes: #1100737)
+  * Drop GNU/kFreeBSD from supported platforms.
+  * Ignore blank lines in pybuild.testfiles. (Closes: #1123536)
+  * Update cpython3_fallback.
+  * Add --test-unittest to explicitly select the unittest runner.
+
+ -- Stefano Rivera <stefanor@debian.org>  Sun, 21 Dec 2025 17:07:09 -0400
+
 dh-python (6.20251204.1) unstable; urgency=medium
 
   * Fix a regression in 6.20251204: Generate dependencies for packages
diff -pruN 6.20251204.1/dh_python3 6.20251221/dh_python3
--- 6.20251204.1/dh_python3	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dh_python3	2025-12-21 21:07:09.000000000 +0000
@@ -27,7 +27,9 @@ import sys
 from argparse import ArgumentParser, SUPPRESS
 from os.path import exists, join
 from shutil import copy as fcopy
-from dhpython.debhelper import DebHelper
+from typing import cast
+
+from dhpython.debhelper import DebHelper, Options
 from dhpython.depends import Dependencies
 from dhpython.interpreter import Interpreter, EXTFILE_RE
 from dhpython.version import supported, default, Version, VersionRange
@@ -37,12 +39,13 @@ from dhpython.option import compiled_reg
 from dhpython.tools import pyinstall, pyremove
 
 # initialize script
-logging.basicConfig(format='%(levelname).1s: dh_python3 '
-                           '%(module)s:%(lineno)d: %(message)s')
-log = logging.getLogger('dhpython')
+logging.basicConfig(
+    format="%(levelname).1s: dh_python3 " "%(module)s:%(lineno)d: %(message)s"
+)
+log = logging.getLogger("dhpython")
 os.umask(0o22)
-DEFAULT = default('cpython3')
-SUPPORTED = supported('cpython3')
+DEFAULT = default("cpython3")
+SUPPORTED = supported("cpython3")
 
 # See /usr/share/doc/debhelper/PROGRAMMING.md.gz
 #
@@ -50,164 +53,236 @@ SUPPORTED = supported('cpython3')
 
 
 class Scanner(Scan):
-    def handle_ext(self, fpath):
-        _, fname = fpath.rsplit('/', 1)
-        tagver = EXTFILE_RE.search(fname)
-        if tagver is None:
+    def handle_ext(self, fpath: str) -> Version | None:
+        _, fname = fpath.rsplit("/", 1)
+        if not (m := EXTFILE_RE.search(fname)):
             # yeah, python3.1 is not covered, but we don't want to
             # mess with non-Python libraries, don't we?
-            return
-        tagver = tagver.groupdict()['ver']
+            return None
+        tagver = m.groupdict()["ver"]
         if tagver is None:
-            return
+            return None
         tagver = Version("%s.%s" % (tagver[0], tagver[1:]))
         return tagver
 
 
-def main():
+def main() -> None:
     parser = ArgumentParser()
+    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
     parser.add_argument(
-        '--version', action='version', version='%(prog)s DEVELV')
-    parser.add_argument(
-        '--no-guessing-deps', action='store_false', dest='guess_deps',
-        help='disable guessing dependencies')
-    parser.add_argument(
-        '--skip-private', action='store_true',
-        help="don't check private directories")
-    parser.add_argument(
-        '-v', '--verbose', action='store_true',
-        default=os.environ.get('DH_VERBOSE') == '1',
-        help='turn verbose mode on')
+        "--no-guessing-deps",
+        action="store_false",
+        dest="guess_deps",
+        help="disable guessing dependencies",
+    )
+    parser.add_argument(
+        "--skip-private", action="store_true", help="don't check private directories"
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        default=os.environ.get("DH_VERBOSE") == "1",
+        help="turn verbose mode on",
+    )
     # arch=False->arch:all only, arch=True->arch:any only, None->all of them
     parser.add_argument(
-        '-i', '--indep', action='store_false', dest='arch', default=None,
-        help='act on architecture independent packages')
-    parser.add_argument(
-        '-a', '-s', '--arch', action='store_true', dest='arch',
-        help='act on architecture dependent packages')
-    parser.add_argument(
-        '-q', '--quiet', action='store_false', dest='verbose', help='be quiet')
-    parser.add_argument(
-        '-p', '--package', action='append', metavar='PACKAGE',
-        help='act on the package named PACKAGE')
-    parser.add_argument(
-        '-N', '--no-package', action='append', metavar='PACKAGE',
-        help='do not act on the specified package')
-    parser.add_argument(
-        '--remaining-packages', action='store_true',
-        help='Do not act on the packages which have already been acted on by '
-             'this debhelper command earlier')
-    parser.add_argument(
-        '--compile-all', action='store_true',
-        help='compile all files from given private directory in postinst, not '
-             'just the ones provided by the package')
-    parser.add_argument(
-        '-V', type=VersionRange, dest='vrange', metavar='[X.Y][-][A.B]',
-        help='specify list of supported Python versions. See py3compile(1) for '
-             'examples')
-    parser.add_argument(
-        '-X', '--exclude', action='append', dest='regexpr', type=compiled_regex,
-        metavar='REGEXPR',
-        help='exclude .py files that match given REGEXPR from '
-             'byte-compilation, in a private dir. You may use this option '
-             'multiple times to build up a list of things to exclude.')
-    parser.add_argument(
-        '--accept-upstream-versions', action='store_true',
-        help='accept upstream versions while translating Python dependencies '
-             'into Debian ones')
-    parser.add_argument(
-        '--depends', action='append', metavar='REQ',
-        help='translate given requirements into Debian dependencies and add '
-             'them to ${python3:Depends}. Use it for missing items in '
-             'requires.txt.')
-    parser.add_argument(
-        '--depends-section', action='append', metavar='SECTION',
-        help='translate requirements from given section into Debian '
-             'dependencies and add them to ${python3:Depends}')
-    parser.add_argument(
-        '--recommends', action='append', metavar='REQ',
-        help='translate given requirements into Debian dependencies and add '
-             'them to ${python3:Recommends}')
-    parser.add_argument(
-        '--recommends-section', action='append', metavar='SECTION',
-        help='translate requirements from given section into Debian '
-             'dependencies and add them to ${python3:Recommends}')
-    parser.add_argument(
-        '--suggests', action='append', metavar='REQ',
-        help='translate given requirements into Debian dependencies and add '
-             'them to ${python3:Suggests}')
-    parser.add_argument(
-        '--suggests-section', action='append', metavar='SECTION',
-        help='translate requirements from given section into Debian '
-             'dependencies and add them to ${python3:Suggests}')
-    parser.add_argument(
-        '--requires', action='append', metavar='FILE',
-        help='translate requirements from given file into Debian dependencies '
-             'and add them to ${python3:Depends}')
-    parser.add_argument(
-        '--shebang', metavar='COMMAND',
-        help='use given command as shebang in scripts')
-    parser.add_argument(
-        '--ignore-shebangs', action='store_true',
-        help='do not translate shebangs into Debian dependencies')
-    parser.add_argument(
-        '--no-dbg-cleaning', action='store_false', dest='clean_dbg_pkg',
-        help='do not remove files from debug packages')
-    parser.add_argument(
-        '--no-ext-rename', action='store_true',
-        help='do not add magic tags nor multiarch tuples to extension file '
-             'names)')
-    parser.add_argument(
-        '--no-shebang-rewrite', action='store_true',
-        help='do not rewrite shebangs')
-    parser.add_argument('private_dir', nargs='?',
-        help='Private directory containing Python modules (optional)')
+        "-i",
+        "--indep",
+        action="store_false",
+        dest="arch",
+        default=None,
+        help="act on architecture independent packages",
+    )
+    parser.add_argument(
+        "-a",
+        "-s",
+        "--arch",
+        action="store_true",
+        dest="arch",
+        help="act on architecture dependent packages",
+    )
+    parser.add_argument(
+        "-q", "--quiet", action="store_false", dest="verbose", help="be quiet"
+    )
+    parser.add_argument(
+        "-p",
+        "--package",
+        action="append",
+        metavar="PACKAGE",
+        help="act on the package named PACKAGE",
+    )
+    parser.add_argument(
+        "-N",
+        "--no-package",
+        action="append",
+        metavar="PACKAGE",
+        help="do not act on the specified package",
+    )
+    parser.add_argument(
+        "--remaining-packages",
+        action="store_true",
+        help="Do not act on the packages which have already been acted on by "
+        "this debhelper command earlier",
+    )
+    parser.add_argument(
+        "--compile-all",
+        action="store_true",
+        help="compile all files from given private directory in postinst, not "
+        "just the ones provided by the package",
+    )
+    parser.add_argument(
+        "-V",
+        type=VersionRange,
+        dest="vrange",
+        metavar="[X.Y][-][A.B]",
+        help="specify list of supported Python versions. See py3compile(1) for "
+        "examples",
+    )
+    parser.add_argument(
+        "-X",
+        "--exclude",
+        action="append",
+        dest="regexpr",
+        type=compiled_regex,
+        metavar="REGEXPR",
+        help="exclude .py files that match given REGEXPR from "
+        "byte-compilation, in a private dir. You may use this option "
+        "multiple times to build up a list of things to exclude.",
+    )
+    parser.add_argument(
+        "--accept-upstream-versions",
+        action="store_true",
+        help="accept upstream versions while translating Python dependencies "
+        "into Debian ones",
+    )
+    parser.add_argument(
+        "--depends",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Depends}. Use it for missing items in "
+        "requires.txt.",
+    )
+    parser.add_argument(
+        "--depends-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Depends}",
+    )
+    parser.add_argument(
+        "--recommends",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Recommends}",
+    )
+    parser.add_argument(
+        "--recommends-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Recommends}",
+    )
+    parser.add_argument(
+        "--suggests",
+        action="append",
+        metavar="REQ",
+        help="translate given requirements into Debian dependencies and add "
+        "them to ${python3:Suggests}",
+    )
+    parser.add_argument(
+        "--suggests-section",
+        action="append",
+        metavar="SECTION",
+        help="translate requirements from given section into Debian "
+        "dependencies and add them to ${python3:Suggests}",
+    )
+    parser.add_argument(
+        "--requires",
+        action="append",
+        metavar="FILE",
+        help="translate requirements from given file into Debian dependencies "
+        "and add them to ${python3:Depends}",
+    )
+    parser.add_argument(
+        "--shebang", metavar="COMMAND", help="use given command as shebang in scripts"
+    )
+    parser.add_argument(
+        "--ignore-shebangs",
+        action="store_true",
+        help="do not translate shebangs into Debian dependencies",
+    )
+    parser.add_argument(
+        "--no-dbg-cleaning",
+        action="store_false",
+        dest="clean_dbg_pkg",
+        help="do not remove files from debug packages",
+    )
+    parser.add_argument(
+        "--no-ext-rename",
+        action="store_true",
+        help="do not add magic tags nor multiarch tuples to extension file " "names)",
+    )
+    parser.add_argument(
+        "--no-shebang-rewrite", action="store_true", help="do not rewrite shebangs"
+    )
+    parser.add_argument(
+        "private_dir",
+        nargs="?",
+        help="Private directory containing Python modules (optional)",
+    )
     # debhelper options:
-    parser.add_argument('-O', action='append', help=SUPPRESS)
+    parser.add_argument("-O", action="append", help=SUPPRESS)
 
-    options = parser.parse_args(os.environ.get('DH_OPTIONS', '').split()
-                                + sys.argv[1:])
+    options = parser.parse_args(os.environ.get("DH_OPTIONS", "").split() + sys.argv[1:])
     if options.O:
         parser.parse_known_args(options.O, options)
 
     private_dir = options.private_dir
     if private_dir:
-        if not private_dir.startswith('/'):
+        if not private_dir.startswith("/"):
             # handle usr/share/foo dirs (without leading slash)
-            private_dir = '/' + private_dir
+            private_dir = "/" + private_dir
     # TODO: support more than one private dir at the same time (see :meth:scan)
     if options.skip_private:
         private_dir = False
 
     if options.verbose:
         log.setLevel(logging.DEBUG)
-        log.debug('version: DEVELV')
-        log.debug('argv: %s', sys.argv)
-        log.debug('options: %s', options)
-        log.debug('supported Python versions: %s (default=%s)',
-                  ','.join(str(v) for v in SUPPORTED), DEFAULT)
+        log.debug("version: DEVELV")
+        log.debug("argv: %s", sys.argv)
+        log.debug("options: %s", options)
+        log.debug(
+            "supported Python versions: %s (default=%s)",
+            ",".join(str(v) for v in SUPPORTED),
+            DEFAULT,
+        )
     else:
         log.setLevel(logging.INFO)
 
     options.write_log = False
-    if os.environ.get('DH_INTERNAL_OVERRIDE', ''):
+    if os.environ.get("DH_INTERNAL_OVERRIDE", ""):
         options.write_log = True
 
     try:
-        dh = DebHelper(options, impl='cpython3')
+        dh = DebHelper(cast(Options, options), impl="cpython3")
     except Exception as e:
-        log.error('cannot initialize DebHelper: %s', e)
+        log.error("cannot initialize DebHelper: %s", e)
         sys.exit(2)
     if not dh.packages:
-        log.error('no package to act on (python3-foo or one with ${python3:Depends} in Depends)')
+        log.error(
+            "no package to act on (python3-foo or one with ${python3:Depends} in Depends)"
+        )
         # sys.exit(7)
     if not options.vrange and dh.python_version:
         options.vrange = VersionRange(dh.python_version)
 
-    interpreter = Interpreter('python3')
+    interpreter = Interpreter("python3")
     for package, _ in dh.packages.items():
-        log.debug('processing package %s...', package)
-        interpreter.debug = package.endswith('-dbg')
+        log.debug("processing package %s...", package)
+        interpreter.debug = package.endswith("-dbg")
 
         if not private_dir:
             try:
@@ -221,33 +296,36 @@ def main():
                 log.error("%s.pyremove: %s", package, err)
                 sys.exit(5)
             fix_locations(package, interpreter, SUPPORTED, options)
-        stats = Scanner(interpreter, package, private_dir, options).result
+        stats = Scanner(interpreter, package, private_dir, options=options).result
 
-        dependencies = Dependencies(package, 'cpython3', dh.build_depends)
+        dependencies = Dependencies(package, "cpython3", dh.build_depends)
         dependencies.parse(stats, options)
 
         pyclean_added = False  # invoke pyclean only once in maintainer script
-        if stats['compile']:
-            args = ''
+        if stats["compile"]:
+            args = ""
             if options.vrange:
                 args += "-V %s" % options.vrange
-            dh.autoscript(package, 'postinst', 'postinst-py3compile', args)
-            dh.autoscript(package, 'prerm', 'prerm-py3clean', '')
+            dh.autoscript(package, "postinst", "postinst-py3compile", args)
+            dh.autoscript(package, "prerm", "prerm-py3clean", "")
             pyclean_added = True
-        for pdir, details in sorted(stats['private_dirs'].items()):
-            if not details.get('compile'):
+        for pdir, details in sorted(stats["private_dirs"].items()):
+            if not details["compile"]:
                 continue
             if not pyclean_added:
-                dh.autoscript(package, 'prerm', 'prerm-py3clean', '')
+                dh.autoscript(package, "prerm", "prerm-py3clean", "")
                 pyclean_added = True
 
             args = pdir
 
-            ext_for = details.get('ext_vers')
-            ext_no_version = details.get('ext_no_version')
-            if ext_for is None and not ext_no_version:  # no extension
-                shebang_versions = list(i.version for i in details.get('shebangs', [])
-                                        if i.version and i.version.minor)
+            ext_for = details["ext_vers"]
+            ext_no_version = details["ext_no_version"]
+            if not ext_for and not ext_no_version:  # no extension
+                shebang_versions = list(
+                    i.version
+                    for i in details["shebangs"]
+                    if i.version and i.version.minor
+                )
                 if not options.ignore_shebangs and len(shebang_versions) == 1:
                     # only one version from shebang
                     args += " -V %s" % shebang_versions[0]
@@ -255,11 +333,15 @@ def main():
                     args += " -V %s" % options.vrange
             elif ext_no_version:
                 # at least one extension's version not detected
-                if options.vrange and '-' not in str(options.vrange):
+                if options.vrange and "-" not in str(options.vrange):
                     ver = str(options.vrange)
                 else:  # try shebang or default Python version
-                    ver = (list(i.version for i in details.get('shebangs', [])
-                                if i.version and i.version.minor) or [None])[0] or DEFAULT
+                    v = DEFAULT
+                    for i in details["shebangs"]:
+                        if i.version and i.version.minor:
+                            v = i.version
+                            break
+                    ver = str(v)
                 dependencies.depend("python%s" % ver)
                 args += " -V %s" % ver
             else:
@@ -270,23 +352,23 @@ def main():
             for regex in options.regexpr or []:
                 args += " -X '%s'" % regex.pattern.replace("'", r"'\''")
 
-            dh.autoscript(package, 'postinst', 'postinst-py3compile', args)
+            dh.autoscript(package, "postinst", "postinst-py3compile", args)
 
         dependencies.export_to(dh)
 
-        pydist_file = join('debian', "%s.pydist" % package)
+        pydist_file = join("debian", "%s.pydist" % package)
 
         if exists(pydist_file):
             if not validate_pydist(pydist_file):
                 log.warning("%s.pydist file is invalid", package)
             else:
-                dstdir = join('debian', package, 'usr/share/python3/dist/')
+                dstdir = join("debian", package, "usr/share/python3/dist/")
                 if not exists(dstdir):
                     os.makedirs(dstdir)
                 fcopy(pydist_file, join(dstdir, package))
-        bcep_file = join('debian', "%s.bcep" % package)
+        bcep_file = join("debian", "%s.bcep" % package)
         if exists(bcep_file):
-            dstdir = join('debian', package, 'usr/share/python3/bcep/')
+            dstdir = join("debian", package, "usr/share/python3/bcep/")
             if not exists(dstdir):
                 os.makedirs(dstdir)
             fcopy(bcep_file, join(dstdir, package))
@@ -294,5 +376,5 @@ def main():
     dh.save()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()
diff -pruN 6.20251204.1/dhpython/__init__.py 6.20251221/dhpython/__init__.py
--- 6.20251204.1/dhpython/__init__.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/__init__.py	2025-12-21 21:07:09.000000000 +0000
@@ -20,61 +20,63 @@
 
 import re
 
-PKG_PREFIX_MAP = {'cpython3': 'python3'}
+PKG_PREFIX_MAP = {"cpython3": "python3"}
 
 # minimum version required for compile/clean scripts:
-MINPYCDEP = {'cpython3': 'python3:any'}
+MINPYCDEP = {"cpython3": "python3:any"}
 
 PUBLIC_DIR_RE = {
-    'cpython3': re.compile(r'.*?/usr/lib/python(3(?:\.\d+)?)(?:/|$)'),
+    "cpython3": re.compile(r".*?/usr/lib/python(3(?:\.\d+)?)(?:/|$)"),
 }
 
 INTERPRETER_DIR_TPLS = {
-    'cpython3': r'.*/python3(?:\.\d+)?/',
+    "cpython3": r".*/python3(?:\.\d+)?/",
 }
 
 MULTIARCH_DIR_TPL = re.compile(
-    '.*/([a-z][^/-]+-(?:linux|kfreebsd|gnu)(?:-[^/-]+)?)(?:/.*|$)')
+    ".*/([a-z][^/-]+-(?:linux|kfreebsd|gnu)(?:-[^/-]+)?)(?:/.*|$)"
+)
 
 # Interpreter site-directories
 OLD_SITE_DIRS = {
-    'cpython3': [
-        '/usr/local/lib/python{}/site-packages',
-        '/usr/local/lib/python{}/dist-packages',
-        '/usr/lib/python{}/site-packages',
-        '/usr/lib/python{}/dist-packages',
-        '/var/lib/python-support/python{}',
-        '/usr/lib/pymodules/python{}'],
+    "cpython3": [
+        "/usr/local/lib/python{}/site-packages",
+        "/usr/local/lib/python{}/dist-packages",
+        "/usr/lib/python{}/site-packages",
+        "/usr/lib/python{}/dist-packages",
+        "/var/lib/python-support/python{}",
+        "/usr/lib/pymodules/python{}",
+    ],
 }
 
 # PyDist related
 PYDIST_DIRS = {
-    'cpython3': '/usr/share/python3/dist/',
+    "cpython3": "/usr/share/python3/dist/",
 }
 
 PYDIST_OVERRIDES_FNAMES = {
-    'cpython3': 'debian/py3dist-overrides',
+    "cpython3": "debian/py3dist-overrides",
 }
 
 PYDIST_DPKG_SEARCH_TPLS = {
     # implementation: (dpkg -S query, regex filter)
-    'cpython3': ('*python3/*/{}-?*.*-info', r'.(egg|dist)-info$'),
+    "cpython3": ("*python3/*/{}-?*.*-info", r".(egg|dist)-info$"),
 }
 
 # DebHelper related
 DEPENDS_SUBSTVARS = {
-    'cpython3': '${python3:Depends}',
+    "cpython3": "${python3:Depends}",
 }
 PKG_NAME_TPLS = {
-    'cpython3': ('python3-', 'python3.'),
+    "cpython3": ("python3-", "python3."),
 }
 RT_LOCATIONS = {
-    'cpython3': '/usr/share/python3/runtime.d/',
+    "cpython3": "/usr/share/python3/runtime.d/",
 }
 RT_TPLS = {
-    'cpython3': '''
+    "cpython3": """
 if [ "$1" = rtupdate ]; then
 \tpy3clean {pkg_arg} {dname}
 \tpy3compile {pkg_arg} {args} {dname}
-fi''',
+fi""",
 }
diff -pruN 6.20251204.1/dhpython/_defaults.py 6.20251221/dhpython/_defaults.py
--- 6.20251204.1/dhpython/_defaults.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/_defaults.py	2025-12-21 21:07:09.000000000 +0000
@@ -24,20 +24,34 @@ from configparser import ConfigParser
 from os import environ
 from os.path import exists
 from subprocess import Popen, PIPE
+from typing import NamedTuple
 
-SUPPORTED = {
-    'cpython3': [(3, 8)],
+
+SUPPORTED: dict[str, tuple[tuple[int, int], ...]] = {
+    "cpython3": ((3, 8),),
 }
-DEFAULT = {
-    'cpython3': (3, 8),
+DEFAULT: dict[str, tuple[int, int]] = {
+    "cpython3": (3, 8),
 }
 
-log = logging.getLogger('dhpython')
+log = logging.getLogger("dhpython")
+
+
+class CPythonVersions(NamedTuple):
+    default: tuple[int, int] | None
+    supported: tuple[tuple[int, int], ...]
 
 
-def cpython_versions(major):
-    result = [None, None]
+def _parse_version(version: str) -> tuple[int, int]:
+    parts = version.split(".")
+    assert len(parts) == 2
+    return (int(parts[0]), int(parts[1]))
+
+
+def cpython_versions(major: int) -> CPythonVersions:
     assert major > 2
+    default_ver: tuple[int, int] | None = None
+    supported_vers: tuple[tuple[int, int], ...] = ()
     ver = str(major)
     supported = environ.get("DEBPYTHON{}_SUPPORTED".format(ver))
     default = environ.get("DEBPYTHON{}_DEFAULT".format(ver))
@@ -45,48 +59,51 @@ def cpython_versions(major):
         config = ConfigParser()
         config.read("/usr/share/python{}/debian_defaults".format(ver))
         if not default:
-            default = config.get('DEFAULT', 'default-version', fallback='')[6:]
+            default = config.get("DEFAULT", "default-version", fallback="")[6:]
         if not supported:
-            supported = config.get('DEFAULT', 'supported-versions', fallback='')\
-                .replace('python', '')
+            supported = config.get(
+                "DEFAULT", "supported-versions", fallback=""
+            ).replace("python", "")
     if default:
         try:
-            result[0] = tuple(int(i) for i in default.split('.'))
+            default_ver = _parse_version(default)
         except Exception as err:
-            log.warning('invalid debian_defaults file: %s', err)
+            log.warning("invalid debian_defaults file: %s", err)
     if supported:
         try:
-            result[1] = tuple(tuple(int(j) for j in i.strip().split('.'))
-                              for i in supported.split(','))
+            supported_vers = tuple(_parse_version(i) for i in supported.split(","))
         except Exception as err:
-            log.warning('invalid debian_defaults file: %s', err)
-    return result
+            log.warning("invalid debian_defaults file: %s", err)
+    return CPythonVersions(
+        default=default_ver,
+        supported=supported_vers,
+    )
 
 
-def from_file(fpath):
+def from_file(fpath: str) -> None:
     if not exists(fpath):
         raise ValueError("missing interpreter: %s" % fpath)
     command = "{} --version".format(fpath)
-    with Popen(command, shell=True, stdout=PIPE) as process:
+    with Popen(command, shell=True, stdout=PIPE, encoding="utf-8") as process:
         stdout, _ = process.communicate()
-        stdout = str(stdout, 'utf-8')
 
     print(stdout)
 
 
 cpython3 = cpython_versions(3)
-if cpython3[0]:
-    DEFAULT['cpython3'] = cpython3[0]
-if cpython3[1]:
-    SUPPORTED['cpython3'] = cpython3[1]
+if cpython3.default:
+    DEFAULT["cpython3"] = cpython3.default
+if cpython3.supported:
+    SUPPORTED["cpython3"] = cpython3.supported
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     import sys
+
     if len(sys.argv) != 3:
-        print('invalid number of arguments', file=sys.stderr)
+        print("invalid number of arguments", file=sys.stderr)
         sys.exit(1)
-    if sys.argv[1] == 'default':
-        print('.'.join(str(i) for i in DEFAULT[sys.argv[2]]))
-    elif sys.argv[1] == 'supported':
-        print(','.join(('.'.join(str(i) for i in v) for v in SUPPORTED[sys.argv[2]])))
+    if sys.argv[1] == "default":
+        print(".".join(str(i) for i in DEFAULT[sys.argv[2]]))
+    elif sys.argv[1] == "supported":
+        print(",".join((".".join(str(i) for i in v) for v in SUPPORTED[sys.argv[2]])))
diff -pruN 6.20251204.1/dhpython/build/base.py 6.20251221/dhpython/build/base.py
--- 6.20251204.1/dhpython/build/base.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/base.py	2025-12-21 21:07:09.000000000 +0000
@@ -19,28 +19,62 @@
 # THE SOFTWARE.
 
 import logging
+from argparse import Namespace
 from functools import wraps
 from glob import glob1
-from os import remove, walk, makedirs
+from os import environ, remove, walk, makedirs
 from os.path import exists, isdir, join
 from pathlib import Path
 from shlex import quote
 from shutil import rmtree, copyfile, copytree, which
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Callable,
+    ClassVar,
+    Concatenate,
+    Literal,
+    ParamSpec,
+    TypeVar,
+    cast,
+    overload,
+)
+
 from dhpython.debhelper import DebHelper, build_options
 from dhpython.exceptions import RequiredCommandMissingException
-from dhpython.tools import execute
-
-log = logging.getLogger('dhpython')
+from dhpython.tools import execute, ExecutionResult, ExecutionResultWithOutput
+from dhpython.build.types import Args, Context
 
+if TYPE_CHECKING:
+    from functools import _Wrapped
 
-def copy_test_files(dest='{build_dir}',
-                    filelist='{home_dir}/testfiles_to_rm_before_install',
-                    add_to_args=('test', 'tests')):
+log = logging.getLogger('dhpython')
 
-    def _copy_test_files(func):
+P = ParamSpec('P')
+T = TypeVar('T')
+BC = TypeVar('BC', bound="Base")
+
+def copy_test_files(
+    dest: str = '{build_dir}',
+    filelist: str = '{home_dir}/testfiles_to_rm_before_install',
+    add_to_args: tuple[str, ...] = ('test', 'tests')
+) -> Callable[
+        [Callable[Concatenate[BC, Context, Args, P], T]],
+        Callable[Concatenate[BC, Context, Args, P], T],
+    ]:
+
+    def _copy_test_files(
+        func: Callable[Concatenate[BC, Context, Args, P], T], /
+    ) -> "_Wrapped[Concatenate[BC, Context, Args, P], T, Concatenate[BC, Context, Args, P], T]":
 
         @wraps(func)
-        def __copy_test_files(self, context, args, *oargs, **kwargs):
+        def __copy_test_files(
+            self: BC,
+            context: Context,
+            args: Args,
+            *oargs: P.args,
+            **kwargs: P.kwargs
+        ) -> T:
             files_to_copy = {'pyproject.toml', 'pytest.ini', 'test', 'tests'}
             # check debian/pybuild_pythonX.Y.testfiles
             for tpl in ('_{i}{v}', '_{i}{m}', ''):
@@ -51,8 +85,14 @@ def copy_test_files(dest='{build_dir}',
                 if exists(fpath):
                     with open(fpath, encoding='utf-8') as fp:
                         # overwrite files_to_copy if .testfiles file found
-                        files_to_copy = [line.strip() for line in fp.readlines()
-                                         if not line.startswith('#')]
+                        files_to_copy = set()
+                        for line in fp.readlines():
+                            line = line.strip()
+                            if line.startswith('#'):
+                                continue
+                            if not line:
+                                continue
+                            files_to_copy.add(line)
                         break
 
             files_to_remove = set()
@@ -100,29 +140,34 @@ class Base:
     :attr SUPPORTED_INTERPRETERS: set of interpreter templates (with or without
         {version}) supported by given plugin
     """
+    NAME: ClassVar[str]
     DESCRIPTION = ''
-    REQUIRED_COMMANDS = []
-    REQUIRED_FILES = []
-    OPTIONAL_FILES = {}
-    SUPPORTED_INTERPRETERS = {'python', 'python3', 'python-dbg', 'python3-dbg',
-                              'python{version}', 'python{version}-dbg'}
+    REQUIRED_COMMANDS: list[str] = []
+    REQUIRED_FILES: list[str] = []
+    DETECTED_REQUIRED_FILES: dict[str, list[str]]
+    OPTIONAL_FILES: dict[str, int] = {}
+    DETECTED_OPTIONAL_FILES: dict[str, list[str]]
+    SUPPORTED_INTERPRETERS: Literal[True] | set[str] = {
+        'python', 'python3', 'python-dbg', 'python3-dbg',
+        'python{version}', 'python{version}-dbg'
+    }
     # files and directories to remove during clean step (other than .pyc):
     CLEAN_FILES = {'.pytest_cache', '.coverage'}
 
-    def __init__(self, cfg):
+    def __init__(self, cfg: Namespace) -> None:
         self.cfg = cfg
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "BuildSystem(%s)" % self.NAME
 
     @classmethod
-    def is_usable(cls):
+    def is_usable(cls) -> None:
         for command in cls.REQUIRED_COMMANDS:
             pth = which(command)
             if not pth:
                 raise RequiredCommandMissingException(command)
 
-    def detect(self, context):
+    def detect(self, context: Context) -> int:
         """Return certainty level that this plugin describes the right build system
 
         This method is using cls.{REQUIRED,OPTIONAL}_FILES only by default,
@@ -159,7 +204,7 @@ class Base:
             return 100
         return result
 
-    def clean(self, context, args):
+    def clean(self, context: Context, args: Args) -> None:
         tox_dir = join(args['dir'], '.tox')
         if isdir(tox_dir):
             try:
@@ -216,17 +261,20 @@ class Base:
                         except Exception:
                             log.debug('cannot remove %s', fpath)
 
-    def configure(self, context, args):
+    def configure(self, context: Context, args: Args) -> None:
         raise NotImplementedError("configure method not implemented in %s" % self.NAME)
 
-    def install(self, context, args):
+    def install(self, context: Context, args: Args) -> None:
         raise NotImplementedError("install method not implemented in %s" % self.NAME)
 
-    def build(self, context, args):
+    def build(self, context: Context, args: Args) -> None:
         raise NotImplementedError("build method not implemented in %s" % self.NAME)
 
+    def test(self, context: Context, args: Args) -> None:
+        raise NotImplementedError("test method not implemented in %s" % self.NAME)
+
     @copy_test_files()
-    def test(self, context, args):
+    def test_cmd(self, context: Context, args: Args) -> str:
         if self.cfg.test_nose2:
             return 'cd {build_dir}; {interpreter} -m nose2 -v {args}'
         elif self.cfg.test_nose:
@@ -255,27 +303,14 @@ class Base:
             if args['autopkgtest']:
                 tox_cmd += ['--skip-pkg-install']
 
-            # --installpkg was added in tox 4. Keep tox 3 support for now,
-            # for backportability
-            r = execute(['tox', '--version', '--quiet'], shell=False)
-            try:
-                major_version = int(r['stdout'].split('.', 1)[0])
-            except ValueError as err:
-                raise Exception(f"tox was installed but broken: stdout='{r['stdout']}', stderr='{r['stderr']}'") from err
-            if major_version < 4:
-                # tox will call pip to install the module. Let it install the
-                # module inside the virtualenv
-                pydistutils_cfg = join(args['home_dir'], '.pydistutils.cfg')
-                if exists(pydistutils_cfg):
-                    remove(pydistutils_cfg)
-            else:
-                if not args['autopkgtest']:
+            if not args['autopkgtest']:
+                wheel = self.built_wheel(context, args)
+                if not wheel:
+                    self.build_wheel(context, args)
                     wheel = self.built_wheel(context, args)
-                    if not wheel:
-                        self.build_wheel(context, args)
-                        wheel = self.built_wheel(context, args)
-                    args['wheel'] = wheel
-                    tox_cmd += ['--installpkg', '{wheel}']
+                assert wheel
+                args['wheel'] = wheel
+                tox_cmd += ['--installpkg', '{wheel}']
 
             tox_cmd.append('{args}')
             return ' '.join(tox_cmd)
@@ -285,18 +320,23 @@ class Base:
                 'stestr --config {dir}/.stestr.conf init;'
                 'PYTHON=python{version} stestr --config {dir}/.stestr.conf run'
             )
+        elif self.cfg.test_unittest:
+            return 'cd {build_dir}; {interpreter} -m unittest discover -v {args}'
         elif self.cfg.test_custom:
             return 'cd {build_dir}; {args}'
         else:
-            # Temporary: Until Python 3.12 is established, and packages without
-            # test suites have explicitly disabled tests.
+            log.warning(
+                "No test runner selected, defaulting to unittest, ignoring "
+                "test discovery problems.\nUse --test-unittest to explicitly "
+                "select the unittest runner.")
+            # As this is the fallback option, we allow 0 discovered tests.
             args['ignore_no_tests'] = True
             return 'cd {build_dir}; {interpreter} -m unittest discover -v {args}'
 
-    def build_wheel(self, context, args):
+    def build_wheel(self, context: Context, args: Args) -> None:
         raise NotImplementedError("build_wheel method not implemented in %s" % self.NAME)
 
-    def built_wheel(self, context, args):
+    def built_wheel(self, context: Context, args: Args) -> str | None:
         """Return the path to any built wheels we can find"""
         # pylint: disable=unused-argument
         wheels = list(Path(args['home_dir']).glob('*.whl'))
@@ -307,7 +347,36 @@ class Base:
             return str(wheels[0])
         return None
 
-    def execute(self, context, args, command, log_file=None):
+    @overload
+    def execute(
+        self,
+        context: Context,
+        args: Args,
+        command: str,
+        *,
+        log_file: None = None,
+    ) -> ExecutionResultWithOutput:
+        ...
+
+    @overload
+    def execute(
+        self,
+        context: Context,
+        args: Args,
+        command: str,
+        *,
+        log_file: str | Literal[False],
+    ) -> ExecutionResult:
+        ...
+
+    def execute(
+        self,
+        context: Context,
+        args: Args,
+        command: str,
+        *,
+        log_file: str | Literal[False] | None = None
+    ) -> ExecutionResult | ExecutionResultWithOutput:
         if log_file is False and self.cfg.really_quiet:
             log_file = None
         command = command.format(**args)
@@ -315,9 +384,9 @@ class Base:
         if 'ENV' in args:
             env.update(args['ENV'])
         log.info(command)
-        return execute(command, context['dir'], env, log_file)
+        return execute(command, cwd=context['dir'], env=env, log_output=log_file)
 
-    def print_args(self, context, args):
+    def print_args(self, context: Context, args: Args) -> None:
         # pylint: disable=unused-argument
         cfg = self.cfg
         if len(cfg.print_args) == 1 and len(cfg.interpreter) == 1 and '{version}' not in cfg.interpreter[0]:
@@ -334,41 +403,117 @@ class Base:
                     print('{} {}: {}'.format(args['interpreter'], i, args.get(i, '')))
 
 
-def shell_command(func):
+def shell_command(
+    func: Callable[Concatenate[BC, Context, Args, P], str | Literal[0]], /
+) -> Callable[Concatenate[BC, Context, Args, P], None]:
 
     @wraps(func)
-    def wrapped_func(self, context, args, *oargs, **kwargs):
+    def wrapped_func(
+        self: BC,
+        context: Context,
+        args: Args,
+        *oargs: P.args,
+        **kwargs: P.kwargs,
+    ) -> None:
         command = kwargs.pop('command', None)
         if not command:
             command = func(self, context, args, *oargs, **kwargs)
             if isinstance(command, int):  # final result
-                return command
+                return
+        assert isinstance(command, str)
         if not command:
             log.warning('missing command '
                      '(plugin=%s, method=%s, interpreter=%s, version=%s)',
                      self.NAME, func.__name__,
                      args.get('interpreter'), args.get('version'))
-            return command
+            return
 
+        log_file: str | Literal[False]
         if self.cfg.quiet:
             log_file = join(args['home_dir'], '{}_cmd.log'.format(func.__name__))
         else:
             log_file = False
 
-        quoted_args = dict((k, quote(v)) if k in ('dir', 'destdir')
-                           or k.endswith('_dir') else (k, v)
-                           for k, v in args.items())
+        quoted_args: dict[str, Any] = {}
+        for k, v in args.items():
+            if k in ('dir', 'destdir') or k.endswith('_dir'):
+                assert isinstance(v, str)
+                quoted_args[k] = quote(v)
+            else:
+                quoted_args[k] = v
         command = command.format(**quoted_args)
 
-        output = self.execute(context, args, command, log_file)
-        if output['returncode'] == 5 and args.get('ignore_no_tests', False):
+        output = self.execute(context, args, command=command, log_file=log_file)
+        if output.returncode == 5 and args.get('ignore_no_tests', False):
             # Temporary hack (see Base.test)
             pass
-        elif output['returncode'] != 0:
-            msg = 'exit code={}: {}'.format(output['returncode'], command)
+        elif output.returncode != 0:
+            msg = f'exit code={output.returncode}: {command}'
             if log_file:
-                msg += '\nfull command log is available in {}'.format(log_file)
+                msg += f'\nfull command log is available in {log_file}'
             raise Exception(msg)
-        return True
 
-    return wrapped_func
+    # Workaround https://github.com/python/typeshed/issues/10653
+    return cast(Callable[Concatenate[BC, Context, Args, P], None], wrapped_func)
+
+
+def get_parallel() -> int:
+    """Determine the allowable level of build parallelism with DEB_BUILD_OPTIONS."""
+    parallel = 1
+    if 'DEB_BUILD_OPTIONS' in environ:
+        for option in environ['DEB_BUILD_OPTIONS'].split():
+            key, assign, value = option.partition('=')
+            if key == "parallel" and assign == "=":
+                parallel = int(value)
+    return parallel
+
+
+def create_setuptools_parallel_cfg(
+    backend: str | None = None
+) -> Callable[
+        [Callable[Concatenate[BC, Context, Args, P], T]],
+        Callable[Concatenate[BC, Context, Args, P], T],
+]:
+    """Enable parallel building of extensions, respecting DEB_BUILD_OPTIONS.
+
+    An adaption of Michał Górny's technique described in
+    https://blogs.gentoo.org/mgorny/2024/03/15/optimizing-parallel-extension-builds-in-pep517-builds
+
+    This function is not part of :pyfunc:`create_pydistutils_cfg` as that
+    function is not used for `build_wheel`, which is our target.
+
+    For PEP-517 (pyproject) builds we can not yet use PEP-517 Config Settings
+    as we need to pass the parallel building option to `build_ext` and
+    setuptools (as of version 80.9.0) does not have a way to redirect PEP-517
+    Config Settings to specific commands. Setuptools only passes PEP-517 Config
+    Settings to all commands via `--global-option` and to `build_wheel` with
+    `--build-option`.
+    See https://github.com/pypa/setuptools/blob/d198e86f57231e83de87975c5c82bc40c196da79/setuptools/build_meta.py
+    and https://github.com/pypa/setuptools/discussions/4083
+    """
+    def decorator(
+        func: Callable[Concatenate[BC, Context, Args, P], T], /
+    ) -> Callable[Concatenate[BC, Context, Args, P], T]:
+        @wraps(func)
+        def wrapped_func(
+            self: BC,
+            context: Context,
+            args: Args,
+            *oargs: P.args,
+            **kwargs: P.kwargs,
+        ) -> T:
+            fpath = join(args['home_dir'], 'setuptools-build_ext-parallel.cfg')
+            if not exists(fpath):
+                with open(fpath, 'w', encoding='utf-8') as fp:
+                    lines = ['[build_ext]\n',
+                             f'parallel={get_parallel()}\n']
+                    log.debug('parallel build extension config file:\n%s', ''.join(lines))
+                    fp.writelines(lines)
+            context['ENV']['DIST_EXTRA_CONFIG'] = fpath
+            return func(self, context, args, *oargs, **kwargs)
+
+        if 'DIST_EXTRA_CONFIG' in environ or (
+                backend is not None and backend != "setuptools.build_meta"):
+            return func
+        return cast(Callable[Concatenate[BC, Context, Args, P], T], wrapped_func)
+    return decorator
diff -pruN 6.20251204.1/dhpython/build/plugin_autopkgtest.py 6.20251221/dhpython/build/plugin_autopkgtest.py
--- 6.20251204.1/dhpython/build/plugin_autopkgtest.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_autopkgtest.py	2025-12-21 21:07:09.000000000 +0000
@@ -19,7 +19,10 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
-from dhpython.build.base import Base, shell_command
+from typing import override
+
+from dhpython.build.base import Base, copy_test_files, shell_command
+from dhpython.build.types import Args, Context
 
 
 class BuildSystem(Base):
@@ -33,6 +36,8 @@ class BuildSystem(Base):
     DESCRIPTION = 'autopkgtest test runner'
     SUPPORTED_INTERPRETERS = {'python3', 'python{version}'}
 
+    @copy_test_files()
     @shell_command
-    def test(self, context, args):
-        return super().test(context, args)
+    @override
+    def test(self, context: Context, args: Args) -> str:
+        return super().test_cmd(context, args)
diff -pruN 6.20251204.1/dhpython/build/plugin_cmake.py 6.20251221/dhpython/build/plugin_cmake.py
--- 6.20251204.1/dhpython/build/plugin_cmake.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_cmake.py	2025-12-21 21:07:09.000000000 +0000
@@ -18,7 +18,10 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
+from typing import override
+
 from dhpython.build.base import Base, shell_command, copy_test_files
+from dhpython.build.types import Args, Context
 
 
 class BuildSystem(Base):
@@ -28,12 +31,14 @@ class BuildSystem(Base):
     OPTIONAL_FILES = {'cmake_uninstall.cmake': 10, 'CMakeCache.txt': 10}
 
     @shell_command
-    def clean(self, context, args):
+    @override
+    def clean(self, context: Context, args: Args) -> str:
         super().clean(context, args)
         return 'dh_auto_clean --buildsystem=cmake'
 
     @shell_command
-    def configure(self, context, args):
+    @override
+    def configure(self, context: Context, args: Args) -> str:
         return ('dh_auto_configure --buildsystem=cmake'
                 ' --builddirectory={build_dir} --'
                 # FindPythonInterp:
@@ -51,21 +56,24 @@ class BuildSystem(Base):
                 ' {args}')
 
     @shell_command
-    def build(self, context, args):
+    @override
+    def build(self, context: Context, args: Args) -> str:
         return ('dh_auto_build --buildsystem=cmake'
                 ' --builddirectory={build_dir}'
                 ' -- {args}')
 
     @shell_command
-    def install(self, context, args):
+    @override
+    def install(self, context: Context, args: Args) -> str:
         return ('dh_auto_install --buildsystem=cmake'
                 ' --builddirectory={build_dir}'
                 ' --destdir={destdir}'
                 ' -- {args}')
 
-    @shell_command
     @copy_test_files()
-    def test(self, context, args):
+    @shell_command
+    @override
+    def test(self, context: Context, args: Args) -> str:
         return ('dh_auto_test --buildsystem=cmake'
                 ' --builddirectory={build_dir}'
                 ' -- {args}')
diff -pruN 6.20251204.1/dhpython/build/plugin_custom.py 6.20251221/dhpython/build/plugin_custom.py
--- 6.20251204.1/dhpython/build/plugin_custom.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_custom.py	2025-12-21 21:07:09.000000000 +0000
@@ -18,7 +18,10 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
+from typing import override
+
 from dhpython.build.base import Base, shell_command, copy_test_files
+from dhpython.build.types import Args, Context
 
 
 class BuildSystem(Base):
@@ -26,23 +29,28 @@ class BuildSystem(Base):
     SUPPORTED_INTERPRETERS = True  # all interpreters
 
     @shell_command
-    def clean(self, context, args):
+    @override
+    def clean(self, context: Context, args: Args) -> str:
         super().clean(context, args)
         return args['args']
 
     @shell_command
-    def configure(self, context, args):
+    @override
+    def configure(self, context: Context, args: Args) -> str:
         return args['args']
 
     @shell_command
-    def build(self, context, args):
+    @override
+    def build(self, context: Context, args: Args) -> str:
         return args['args']
 
     @shell_command
-    def install(self, context, args):
+    @override
+    def install(self, context: Context, args: Args) -> str:
         return args['args']
 
-    @shell_command
     @copy_test_files()
-    def test(self, context, args):
-        return args['args'] or super().test(context, args)
+    @shell_command
+    @override
+    def test(self, context: Context, args: Args) -> str:
+        return args['args'] or super().test_cmd(context, args)
diff -pruN 6.20251204.1/dhpython/build/plugin_distutils.py 6.20251221/dhpython/build/plugin_distutils.py
--- 6.20251204.1/dhpython/build/plugin_distutils.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_distutils.py	2025-12-21 21:07:09.000000000 +0000
@@ -19,17 +19,38 @@
 # THE SOFTWARE.
 
 import logging
+from functools import wraps
 from glob import glob1
 from os import remove
 from os.path import exists, isdir, join
 from shutil import rmtree, move
-from dhpython.build.base import Base, shell_command, copy_test_files
+from typing import (
+    TYPE_CHECKING,
+    Callable,
+    Concatenate,
+    Literal,
+    ParamSpec,
+    TypeVar,
+    override
+)
+
+from dhpython.build.base import Base, shell_command, copy_test_files, create_setuptools_parallel_cfg
+from dhpython.build.types import Args, Context
+
+if TYPE_CHECKING:
+    from functools import _Wrapped
+
 
 log = logging.getLogger('dhpython')
 _setup_tpl = 'setup.py|setup-3.py'
 
 
-def create_pydistutils_cfg(func):
+P = ParamSpec('P')
+T = TypeVar('T')
+
+def create_pydistutils_cfg(
+    func: Callable[Concatenate["BuildSystem", Context, Args, P], T]
+) -> "_Wrapped[Concatenate[BuildSystem, Context, Args, P], T, Concatenate[BuildSystem, Context, Args, P], T]":
     """distutils doesn't have sane command-line API - this decorator creates
     .pydistutils.cfg file to workaround it
 
@@ -37,7 +58,14 @@ def create_pydistutils_cfg(func):
     distutils/setuptools/distribute sources
     """
 
-    def wrapped_func(self, context, args, *oargs, **kwargs):
+    @wraps(func)
+    def wrapped_func(
+        self: "BuildSystem",
+        context: Context,
+        args: Args,
+        *oargs: P.args,
+        **kwargs: P.kwargs
+    ) -> T:
         fpath = join(args['home_dir'], '.pydistutils.cfg')
         if not exists(fpath):
             with open(fpath, 'w', encoding='utf-8') as fp:
@@ -56,7 +84,6 @@ def create_pydistutils_cfg(func):
         context['ENV']['HOME'] = args['home_dir']
         return func(self, context, args, *oargs, **kwargs)
 
-    wrapped_func.__name__ = func.__name__
     return wrapped_func
 
 
@@ -71,7 +98,8 @@ class BuildSystem(Base):
                       '*.egg-info': 10}
     CLEAN_FILES = Base.CLEAN_FILES | {'build'}
 
-    def detect(self, context):
+    @override
+    def detect(self, context: Context) -> int:
         result = super().detect(context)
         if _setup_tpl in self.DETECTED_REQUIRED_FILES:
             context['args']['setup_py'] = self.DETECTED_REQUIRED_FILES[_setup_tpl][0]
@@ -81,7 +109,8 @@ class BuildSystem(Base):
 
     @shell_command
     @create_pydistutils_cfg
-    def clean(self, context, args):
+    @override
+    def clean(self, context: Context, args: Args) -> str | Literal[0]:
         super().clean(context, args)
         if exists(args['interpreter'].binary()):
             return '{interpreter} {setup_py} clean {args}'
@@ -89,20 +118,23 @@ class BuildSystem(Base):
 
     @shell_command
     @create_pydistutils_cfg
-    def configure(self, context, args):
+    @override
+    def configure(self, context: Context, args: Args) -> str:
         return '{interpreter} {setup_py} config {args}'
 
     @shell_command
     @create_pydistutils_cfg
-    def build(self, context, args):
+    @override
+    def build(self, context: Context, args: Args) -> str:
         return '{interpreter.binary_dv} {setup_py} build {args}'
 
     @shell_command
-    def _bdist_wheel(self, context, args):
+    @create_setuptools_parallel_cfg()
+    def _bdist_wheel(self, context: Context, args: Args) -> str:
         # pylint: disable=unused-argument
         try:
             # pylint: disable=unused-import
-            import wheel
+            import wheel  # type: ignore
         except ImportError:
             raise Exception("wheel is required to build wheels for distutils/setuptools packages. Build-Depend on python3-wheel.")
         # Don't build a wheel in the deb install layout
@@ -110,7 +142,8 @@ class BuildSystem(Base):
         remove(fpath)
         return '{interpreter.binary_dv} -c "import setuptools, runpy; runpy.run_path(\'{setup_py}\')" bdist_wheel {args}'
 
-    def build_wheel(self, context, args):
+    @override
+    def build_wheel(self, context: Context, args: Args) -> None:
         self._bdist_wheel(context, args)
         dist_dir = join(args['dir'], 'dist')
         wheels = glob1(dist_dir, '*.whl')
@@ -121,7 +154,8 @@ class BuildSystem(Base):
 
     @shell_command
     @create_pydistutils_cfg
-    def install(self, context, args):
+    @override
+    def install(self, context: Context, args: Args) -> str:
         # remove egg-info dirs from build_dir
         for fname in glob1(args['build_dir'], '*.egg-info'):
             fpath = join(args['build_dir'], fname)
@@ -135,5 +169,6 @@ class BuildSystem(Base):
     @shell_command
     @create_pydistutils_cfg
     @copy_test_files()
-    def test(self, context, args):
-        return super().test(context, args)
+    @override
+    def test(self, context: Context, args: Args) -> str:
+        return super().test_cmd(context, args)
diff -pruN 6.20251204.1/dhpython/build/plugin_meson.py 6.20251221/dhpython/build/plugin_meson.py
--- 6.20251204.1/dhpython/build/plugin_meson.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_meson.py	2025-12-21 21:07:09.000000000 +0000
@@ -19,8 +19,10 @@
 # THE SOFTWARE.
 
 from os.path import join
+from typing import override
 
 from dhpython.build.base import Base, shell_command, copy_test_files
+from dhpython.build.types import Args, Context
 
 
 class BuildSystem(Base):
@@ -29,12 +31,14 @@ class BuildSystem(Base):
     REQUIRED_FILES = ['meson.build']
 
     @shell_command
-    def clean(self, context, args):
+    @override
+    def clean(self, context: Context, args: Args) -> str:
         super().clean(context, args)
         return 'dh_auto_clean --buildsystem=cmake'
 
     @shell_command
-    def configure(self, context, args):
+    @override
+    def configure(self, context: Context, args: Args) -> str:
         # Can't be specified on the command line, directly
         # https://github.com/mesonbuild/meson/issues/9671
         with open(join(args['build_dir'], 'pybuild-meson-native.ini'), 'w', encoding="UTF-8") as f:
@@ -47,21 +51,24 @@ class BuildSystem(Base):
                 ' {args}')
 
     @shell_command
-    def build(self, context, args):
+    @override
+    def build(self, context: Context, args: Args) -> str:
         return ('dh_auto_build --buildsystem=meson'
                 ' --builddirectory={build_dir}'
                 ' -- {args}')
 
     @shell_command
-    def install(self, context, args):
+    @override
+    def install(self, context: Context, args: Args) -> str:
         return ('dh_auto_install --buildsystem=meson'
                 ' --builddirectory={build_dir}'
                 ' --destdir={destdir}'
                 ' -- {args}')
 
-    @shell_command
     @copy_test_files()
-    def test(self, context, args):
+    @shell_command
+    @override
+    def test(self, context: Context, args: Args) -> str:
         return ('dh_auto_test --buildsystem=meson'
                 ' --builddirectory={build_dir}'
                 ' -- {args}')
diff -pruN 6.20251204.1/dhpython/build/plugin_pyproject.py 6.20251221/dhpython/build/plugin_pyproject.py
--- 6.20251204.1/dhpython/build/plugin_pyproject.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/build/plugin_pyproject.py	2025-12-21 21:07:09.000000000 +0000
@@ -22,6 +22,7 @@
 
 from contextlib import contextmanager
 from pathlib import Path
+from typing import Iterator, override
 import logging
 import os.path as osp
 import shutil
@@ -30,7 +31,7 @@ try:
     import tomllib
 except ModuleNotFoundError:
     try:
-        import tomli as tomllib
+        import tomli as tomllib  # type: ignore
     except ModuleNotFoundError:
         # Plugin still works, only needed for autodetection
         pass
@@ -40,14 +41,33 @@ try:
     from installer.sources import WheelFile
     from installer.utils import parse_metadata_file
 except ModuleNotFoundError:
-    SchemeDictionaryDestination = WheelFile = install = parse_metadata_file = None
+    SchemeDictionaryDestination = WheelFile = install = parse_metadata_file = None  # type: ignore
 
-from dhpython.build.base import Base, shell_command
+from dhpython.build.base import Base, shell_command, create_setuptools_parallel_cfg
+from dhpython.build.types import Context, Args
 from dhpython.tools import dpkg_architecture
 
 log = logging.getLogger('dhpython')
 
 
+def _build_backend() -> str | None:
+    """Retrieve the build-system from pyproject.toml, where possible"""
+    try:
+        with open('pyproject.toml', 'rb') as f:
+            pyproject = tomllib.load(f)
+        backend = pyproject.get('build-system', {}).get('build-backend')
+        if backend is None:
+            return None
+        assert isinstance(backend, str)
+        return backend
+    except NameError:
+        # No toml, no autdetection
+        return None
+    except FileNotFoundError:
+        # Not a PEP517 package
+        return None
+
+
 class BuildSystem(Base):
     DESCRIPTION = 'Generic PEP517 build system'
     SUPPORTED_INTERPRETERS = {'python3', 'python{version}'}
@@ -55,7 +75,8 @@ class BuildSystem(Base):
     OPTIONAL_FILES = {}
     CLEAN_FILES = Base.CLEAN_FILES | {'build'}
 
-    def detect(self, context):
+    @override
+    def detect(self, context: Context) -> int:
         """Return certainty level that this plugin describes the right build
         system
 
@@ -70,48 +91,36 @@ class BuildSystem(Base):
         # Temporarily reduce the threshold while we're in beta
         result -= 20
 
-        if self._build_backend():
+        if _build_backend():
             result += 10
 
         if result > 100:
             return 100
         return result
 
-    def _build_backend(self):
-        """Retrieve the build-system from pyproject.toml, where possible"""
-        try:
-            with open('pyproject.toml', 'rb') as f:
-                pyproject = tomllib.load(f)
-            return pyproject.get('build-system', {}).get('build-backend')
-        except NameError:
-            # No toml, no autdetection
-            return None
-        except FileNotFoundError:
-            # Not a PEP517 package
-            return None
-
-    def clean(self, context, args):
+    @override
+    def clean(self, context: Context, args: Args) -> None:
         super().clean(context, args)
         if osp.exists(args['interpreter'].binary()):
             log.debug("removing '%s' (and everything under it)",
                       args['build_dir'])
             if osp.isdir(args['build_dir']):
                 shutil.rmtree(args['build_dir'])
-        return 0  # no need to invoke anything
 
-    def configure(self, context, args):
+    @override
+    def configure(self, context: Context, args: Args) -> None:
         if install is None:
             raise Exception("PEP517 plugin dependencies are not available. "
                             "Please Build-Depend on pybuild-plugin-pyproject.")
         # No separate configure step
-        return 0
 
-    def build(self, context, args):
+    @override
+    def build(self, context: Context, args: Args) -> None:
         self.build_wheel(context, args)
         self.unpack_wheel(context, args)
 
-    def _backend_config_settings(self):
-        backend = self._build_backend()
+    def _backend_config_settings(self) -> list[str]:
+        backend = _build_backend()
         if backend == "mesonpy":
             arch_data = dpkg_architecture()
             return [
@@ -126,7 +135,9 @@ class BuildSystem(Base):
         return []
 
     @shell_command
-    def build_wheel(self, context, args):
+    @create_setuptools_parallel_cfg(backend=_build_backend())
+    @override
+    def build_wheel(self, context: Context, args: Args) -> str:
         """ build a wheel using the PEP517 builder defined by upstream """
         log.info('Building wheel for %s with "build" module',
                  args['interpreter'])
@@ -142,27 +153,30 @@ class BuildSystem(Base):
                )
 
     @contextmanager
-    def opened_wheel(self, context, args):
-        wheel = Path(self.built_wheel(context, args))
+    def opened_wheel(self, context: Context, args: Args) -> Iterator[WheelFile]:
+        wheel_fn = self.built_wheel(context, args)
+        if not wheel_fn:
+            raise AssertionError("Unable to locate a built wheel")
+        wheel = Path(wheel_fn)
         if wheel.name.startswith('UNKNOWN'):
             raise Exception(f'UNKNOWN wheel found: {wheel.name}. Does '
                             'pyproject.toml specify a build-backend?')
         with WheelFile.open(wheel) as source:
             yield source
 
-    def unpack_wheel(self, context, args):
+    def unpack_wheel(self, context: Context, args: Args) -> None:
         """ unpack the wheel into pybuild's normal  """
         log.info('Unpacking wheel built for %s with "installer" module',
                  args['interpreter'])
         extras = {}
         for extra in ('scripts', 'data', 'include'):
             path = Path(args["home_dir"]) / extra
-            if osp.exists(path):
+            if path.exists():
                 log.warning('%s directory already exists, skipping unpack. '
                             'Is the Python package being built twice?',
                             extra.title())
                 return
-            extras[extra] = path
+            extras[extra] = str(path)
         destination = SchemeDictionaryDestination(
             {
                 'platlib': args['build_dir'],
@@ -182,7 +196,8 @@ class BuildSystem(Base):
                 additional_metadata={},
             )
 
-    def install(self, context, args):
+    @override
+    def install(self, context: Context, args: Args) -> None:
         log.info('Copying package built for %s to destdir',
                  args['interpreter'])
         try:
@@ -228,9 +243,10 @@ class BuildSystem(Base):
         )
 
     @shell_command
-    def test(self, context, args):
+    @override
+    def test(self, context: Context, args: Args) -> str:
         scripts = Path(args["home_dir"]) / 'scripts'
         if scripts.exists():
             context['ENV']['PATH'] = f"{scripts}:{context['ENV']['PATH']}"
         context['ENV']['HOME'] = args['home_dir']
-        return super().test(context, args)
+        return super().test_cmd(context, args)
diff -pruN 6.20251204.1/dhpython/build/types.py 6.20251221/dhpython/build/types.py
--- 6.20251204.1/dhpython/build/types.py	1970-01-01 00:00:00.000000000 +0000
+++ 6.20251221/dhpython/build/types.py	2025-12-21 21:07:09.000000000 +0000
@@ -0,0 +1,52 @@
+# Copyright © 2025 Stefano Rivera <stefanor@debian.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from typing import NotRequired, TypedDict
+
+from dhpython.interpreter import Interpreter
+from dhpython.version import Version
+
+
+class Args(TypedDict):
+    ENV: dict[str, str]
+    args: str
+    autopkgtest: bool
+    build_dir: str
+    destdir: str
+    dir: str
+    home_dir: str
+    install_dir: str
+    interpreter: Interpreter
+    package: str
+    version: Version
+    # during tests only:
+    test_dir: NotRequired[str]
+    # Injected by base:
+    ignore_no_tests: NotRequired[bool]
+    wheel: NotRequired[str]
+    # Injected by plugin_distutils:
+    setup_py: NotRequired[str]
+
+
+class Context(TypedDict):
+    ENV: dict[str, str]
+    args: Args
+    dir: str
+    destdir: str
diff -pruN 6.20251204.1/dhpython/debhelper.py 6.20251221/dhpython/debhelper.py
--- 6.20251204.1/dhpython/debhelper.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/debhelper.py	2025-12-21 21:07:09.000000000 +0000
@@ -24,60 +24,94 @@ import re
 from os import makedirs, chmod, environ
 from os.path import basename, exists, join, dirname
 from sys import argv
+from typing import NamedTuple, TypedDict
+
 from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS
 
-log = logging.getLogger('dhpython')
-parse_dep = re.compile(r'''[,\s]*
+
+log = logging.getLogger("dhpython")
+parse_dep = re.compile(
+    r"""[,\s]*
     (?P<name>[^\s:]+)(?::any)?
     \s*
     \(?(?P<version>([>=<]{2,}|=)\s*[^\)]+)?\)?
     \s*
     (?:\[(?P<arch>[^\]]+)\])?
-    ''', re.VERBOSE).match
+    """,
+    re.VERBOSE,
+).match
+
+
+class PackageData(TypedDict):
+    substvars: dict[str, list[str]]
+    autoscripts: dict[str, dict[str, list[str]]]
+    rtupdates: list[tuple[str, str]]
+    arch: str
+
+
+class Options(NamedTuple):
+    arch: bool | None
+    package: list[str]
+    no_package: list[str]
+    write_log: bool
+    compile_all: bool
+    remaining_packages: bool
+
+
+def build_options(
+    *,
+    arch: bool | None = None,
+    package: list[str] | None = None,
+    no_package: list[str] | None = None,
+    write_log: bool = False,
+    compile_all: bool = False,
+    remaining_packages: bool = False,
+) -> Options:
+    return Options(
+        arch=arch,
+        compile_all=compile_all,
+        package=package or [],
+        no_package=no_package or [],
+        write_log=write_log,
+        remaining_packages=remaining_packages,
+    )
 
 
-def build_options(**options):
-    """Build an Options object from kw options"""
-    default_options = {
-        'arch': None,
-        'package': [],
-        'no_package': [],
-        'write_log': False,
-        'remaining_packages': False,
-    }
-    built_options = default_options
-    built_options.update(options)
-    return type('Options', (object,), built_options)
+type BD = dict[str, dict[str | None, str]]
 
 
 class DebHelper:
     """Reinvents the wheel / some dh functionality (Perl is ugly ;-P)"""
 
-    def __init__(self, options, impl='cpython3'):
+    options: Options
+    packages: dict[str, PackageData]
+    build_depends: BD
+
+    def __init__(self, options: Options, impl: str = "cpython3") -> None:
         self.options = options
         self.packages = {}
         self.build_depends = {}
         self.python_version = None
         self.impl = impl
         self.command = {
-            'cpython3': 'dh_python3',
+            "cpython3": "dh_python3",
         }[impl]
-        skip_tpl = set()
+        skip_tpl_set: set[str] = set()
         for name, tpls in PKG_NAME_TPLS.items():
             if name != impl:
-                skip_tpl.update(tpls)
-        skip_tpl = tuple(skip_tpl)
+                skip_tpl_set.update(tpls)
+        skip_tpl = tuple(skip_tpl_set)
         substvar = DEPENDS_SUBSTVARS[impl]
 
         pkgs = options.package
         skip_pkgs = options.no_package
 
         try:
-            with open('debian/control', 'r', encoding='utf-8') as fp:
-                paragraphs = [{}]
+            with open("debian/control", "r", encoding="utf-8") as fp:
+                paragraphs: list[dict[str, str]] = [{}]
                 field = None
                 for lineno, line in enumerate(fp, 1):
-                    if line.startswith('#'):
+                    if line.startswith("#"):
                         continue
                     if not line.strip():
                         if paragraphs[-1]:
@@ -85,75 +119,84 @@ class DebHelper:
                             field = None
                         continue
                     if line[0].isspace():  # Continuation
+                        assert field
                         paragraphs[-1][field] += line.rstrip()
                         continue
-                    if not ':' in line:
+                    if not ":" in line:
                         raise Exception(
-                            'Unable to parse line %i in debian/control: %s'
-                            % (lineno, line))
-                    field, value = line.split(':', 1)
+                            "Unable to parse line %i in debian/control: %s"
+                            % (lineno, line)
+                        )
+                    field, value = line.split(":", 1)
                     field = field.lower()
                     paragraphs[-1][field] = value.strip()
         except IOError:
-            raise Exception('cannot find debian/control file')
+            raise Exception("cannot find debian/control file")
 
         # Trailing new lines?
         if not paragraphs[-1]:
             paragraphs.pop()
 
         if len(paragraphs) < 2:
-            raise Exception('Unable to parse debian/control, found less than '
-                            '2 paragraphs')
+            raise Exception(
+                "Unable to parse debian/control, found less than 2 paragraphs"
+            )
+
+        self.source_name = paragraphs[0]["source"]
+        if self.impl == "cpython3" and "x-python3-version" in paragraphs[0]:
+            self.python_version = paragraphs[0]["x-python3-version"]
+            if len(self.python_version.split(",")) > 2:
+                raise ValueError(
+                    "too many arguments provided for "
+                    "X-Python3-Version: min and max only."
+                )
 
-        self.source_name = paragraphs[0]['source']
-        if self.impl == 'cpython3' and 'x-python3-version' in paragraphs[0]:
-            self.python_version = paragraphs[0]['x-python3-version']
-            if len(self.python_version.split(',')) > 2:
-                raise ValueError('too many arguments provided for '
-                                 'X-Python3-Version: min and max only.')
-
-        build_depends = []
-        for field in ('build-depends', 'build-depends-indep',
-                      'build-depends-arch'):
+        build_depends_list = []
+        for field in ("build-depends", "build-depends-indep", "build-depends-arch"):
             if field in paragraphs[0]:
-                build_depends.append(paragraphs[0][field])
-        build_depends = ', '.join(build_depends)
-        for dep1 in build_depends.split(','):
-            for dep2 in dep1.split('|'):
-                details = parse_dep(dep2)
-                if details:
-                    details = details.groupdict()
-                    if details['arch']:
-                        architectures = details['arch'].split()
+                build_depends_list.append(paragraphs[0][field])
+        build_depends = ", ".join(build_depends_list)
+        for dep1 in build_depends.split(","):
+            for dep2 in dep1.split("|"):
+                if m := parse_dep(dep2):
+                    details = m.groupdict()
+                    if details["arch"]:
+                        architectures = details["arch"].split()
                     else:
                         architectures = [None]
                     for arch in architectures:
-                        self.build_depends.setdefault(
-                            details['name'], {})[arch] = details['version']
+                        self.build_depends.setdefault(details["name"], {})[arch] = (
+                            details["version"]
+                        )
 
         for paragraph_no, paragraph in enumerate(paragraphs[1:], 2):
-            if 'package' not in paragraph:
-                raise Exception('Unable to parse debian/control, paragraph %i '
-                                'missing Package field' % paragraph_no)
-            binary_package = paragraph['package']
+            if "package" not in paragraph:
+                raise Exception(
+                    "Unable to parse debian/control, paragraph %i "
+                    "missing Package field" % paragraph_no
+                )
+            binary_package = paragraph["package"]
             if skip_tpl and binary_package.startswith(skip_tpl):
-                log.debug('skipping package: %s', binary_package)
+                log.debug("skipping package: %s", binary_package)
                 continue
             if pkgs and binary_package not in pkgs:
                 continue
             if skip_pkgs and binary_package in skip_pkgs:
                 continue
-            if (options.remaining_packages and
-                    self.has_acted_on_package(binary_package)):
+            if options.remaining_packages and self.has_acted_on_package(binary_package):
                 continue
-            pkg = {
-                'substvars': {},
-                'autoscripts': {},
-                'rtupdates': [],
-                'arch': paragraph['architecture'],
-            }
-            if (options.arch is False and pkg['arch'] != 'all' or
-                    options.arch is True and pkg['arch'] == 'all'):
+            pkg = PackageData(
+                substvars={},
+                autoscripts={},
+                rtupdates=[],
+                arch=paragraph["architecture"],
+            )
+            if (
+                options.arch is False
+                and pkg["arch"] != "all"
+                or options.arch is True
+                and pkg["arch"] == "all"
+            ):
                 # TODO: check also if arch matches current architecture:
                 continue
 
@@ -161,23 +204,28 @@ class DebHelper:
                 # package doesn't have common prefix (python3-)
                 # so lets check if Depends/Recommends contains the
                 # appropriate substvar
-                if (substvar not in paragraph.get('depends', '')
-                        and substvar not in paragraph.get('recommends', '')):
-                    log.debug('skipping package %s (missing %s in '
-                              'Depends/Recommends)',
-                              binary_package, substvar)
+                if substvar not in paragraph.get(
+                    "depends", ""
+                ) and substvar not in paragraph.get("recommends", ""):
+                    log.debug(
+                        "skipping package %s (missing %s in Depends/Recommends)",
+                        binary_package,
+                        substvar,
+                    )
                     continue
             # Operate on binary_package
             self.packages[binary_package] = pkg
 
         fp.close()
-        log.debug('source=%s, binary packages=%s', self.source_name,
-                  list(self.packages.keys()))
+        log.debug(
+            "source=%s, binary packages=%s",
+            self.source_name,
+            list(self.packages.keys()),
+        )
 
-    def has_acted_on_package(self, package):
+    def has_acted_on_package(self, package: str) -> bool:
         try:
-            with open('debian/{}.debhelper.log'.format(package),
-                      encoding='utf-8') as f:
+            with open("debian/{}.debhelper.log".format(package), encoding="utf-8") as f:
                 for line in f:
                     if line.strip() == self.command:
                         return True
@@ -186,79 +234,81 @@ class DebHelper:
                 raise
         return False
 
-    def addsubstvar(self, package, name, value):
+    def addsubstvar(self, package: str, name: str, value: str) -> None:
         """debhelper's addsubstvar"""
-        self.packages[package]['substvars'].setdefault(name, []).append(value)
+        self.packages[package]["substvars"].setdefault(name, []).append(value)
 
-    def autoscript(self, package, when, template, args):
+    def autoscript(self, package: str, when: str, template: str, args: str) -> None:
         """debhelper's autoscript"""
-        self.packages[package]['autoscripts'].setdefault(when, {})\
-            .setdefault(template, []).append(args)
+        self.packages[package]["autoscripts"].setdefault(when, {}).setdefault(
+            template, []
+        ).append(args)
 
-    def add_rtupdate(self, package, value):
-        self.packages[package]['rtupdates'].append(value)
+    def add_rtupdate(self, package: str, value: tuple[str, str]) -> None:
+        self.packages[package]["rtupdates"].append(value)
 
-    def save_autoscripts(self):
+    def save_autoscripts(self) -> None:
         for package, settings in self.packages.items():
-            autoscripts = settings.get('autoscripts')
+            autoscripts = settings.get("autoscripts")
             if not autoscripts:
                 continue
 
             for when, templates in autoscripts.items():
                 fn = "debian/%s.%s.debhelper" % (package, when)
                 if exists(fn):
-                    with open(fn, 'r', encoding='utf-8') as datafile:
+                    with open(fn, "r", encoding="utf-8") as datafile:
                         data = datafile.read()
                 else:
-                    data = ''
+                    data = ""
 
-                new_data = ''
+                new_data = ""
                 for tpl_name, args in templates.items():
                     for i in args:
                         # try local one first (useful while testing dh_python3)
-                        fpath = join(dirname(__file__), '..',
-                                     "autoscripts/%s" % tpl_name)
+                        fpath = join(
+                            dirname(__file__), "..", "autoscripts/%s" % tpl_name
+                        )
                         if not exists(fpath):
                             fpath = "/usr/share/debhelper/autoscripts/%s" % tpl_name
-                        with open(fpath, 'r', encoding='utf-8') as tplfile:
+                        with open(fpath, "r", encoding="utf-8") as tplfile:
                             tpl = tplfile.read()
                         if self.options.compile_all and args:
                             # TODO: should args be checked to contain dir name?
-                            tpl = tpl.replace('-p #PACKAGE#', '')
-                        elif settings['arch'] == 'all':
-                            tpl = tpl.replace('#PACKAGE#', package)
+                            tpl = tpl.replace("-p #PACKAGE#", "")
+                        elif settings["arch"] == "all":
+                            tpl = tpl.replace("#PACKAGE#", package)
                         else:
-                            arch = environ['DEB_HOST_ARCH']
-                            tpl = tpl.replace('#PACKAGE#', '%s:%s' % (package, arch))
-                        tpl = tpl.replace('#ARGS#', i)
+                            arch = environ["DEB_HOST_ARCH"]
+                            tpl = tpl.replace("#PACKAGE#", "%s:%s" % (package, arch))
+                        tpl = tpl.replace("#ARGS#", i)
                         if tpl not in data and tpl not in new_data:
                             new_data += "\n%s" % tpl
                 if new_data:
-                    data += '\n# Automatically added by {}'.format(basename(argv[0])) +\
-                            '{}\n# End automatically added section\n'.format(new_data)
-                    with open(fn, 'w', encoding='utf-8') as fp:
+                    data += "\n# Automatically added by {}".format(
+                        basename(argv[0])
+                    ) + "{}\n# End automatically added section\n".format(new_data)
+                    with open(fn, "w", encoding="utf-8") as fp:
                         fp.write(data)
 
-    def save_substvars(self):
+    def save_substvars(self) -> None:
         for package, settings in self.packages.items():
-            substvars = settings.get('substvars')
+            substvars = settings.get("substvars")
             if not substvars:
                 continue
             fn = "debian/%s.substvars" % package
             if exists(fn):
-                with open(fn, 'r', encoding='utf-8') as datafile:
+                with open(fn, "r", encoding="utf-8") as datafile:
                     data = datafile.read()
             else:
-                data = ''
+                data = ""
             for name, values in substvars.items():
                 p = data.find("%s=" % name)
                 if p > -1:  # parse the line and remove it from data
-                    e = data[p:].find('\n')
-                    line = data[p + len("%s=" % name):
-                                p + e if e > -1 else None]
-                    items = [i.strip() for i in line.split(',') if i]
-                    if e > -1 and data[p + e:].strip():
-                        data = "%s\n%s" % (data[:p], data[p + e:])
+                    e = data[p:].find("\n")
+                    line = data[p + len("%s=" % name) : p + e if e > -1 else None]
+                    items = [i.strip() for i in line.split(",") if i]
+                    if e > -1 and data[p + e :].strip():
+                        data = "%s\n%s" % (data[:p], data[p + e :])
                     else:
                         data = data[:p]
                 else:
@@ -268,48 +318,47 @@ class DebHelper:
                         items.append(j)
                 if items:
                     if data:
-                        data += '\n'
-                    data += "%s=%s\n" % (name, ', '.join(items))
-            data = data.replace('\n\n', '\n')
+                        data += "\n"
+                    data += "%s=%s\n" % (name, ", ".join(items))
+            data = data.replace("\n\n", "\n")
             if data:
-                with open(fn, 'w', encoding='utf-8') as fp:
+                with open(fn, "w", encoding="utf-8") as fp:
                     fp.write(data)
 
-    def save_rtupdate(self):
+    def save_rtupdate(self) -> None:
         for package, settings in self.packages.items():
-            pkg_arg = '' if self.options.compile_all else "-p %s" % package
-            values = settings.get('rtupdates')
+            pkg_arg = "" if self.options.compile_all else "-p %s" % package
+            values = settings.get("rtupdates")
             if not values:
                 continue
-            d = 'debian/{}/{}'.format(package, RT_LOCATIONS[self.impl])
+            d = "debian/{}/{}".format(package, RT_LOCATIONS[self.impl])
             if not exists(d):
                 makedirs(d)
             fn = "%s/%s.rtupdate" % (d, package)
             if exists(fn):
-                with open(fn, 'r', encoding='utf-8') as fp:
+                with open(fn, "r", encoding="utf-8") as fp:
                     data = fp.read()
             else:
                 data = "#! /bin/sh\nset -e"
             for dname, args in values:
-                cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg,
-                                                dname=dname,
-                                                args=args)
+                cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg, dname=dname, args=args)
                 if cmd not in data:
                     data += "\n%s" % cmd
             if data:
-                with open(fn, 'w', encoding='utf-8') as fp:
+                with open(fn, "w", encoding="utf-8") as fp:
                     fp.write(data)
                 chmod(fn, 0o755)
 
-    def save_log(self):
+    def save_log(self) -> None:
         if not self.options.write_log:
             return
         for package, _ in self.packages.items():
-            with open('debian/{}.debhelper.log'.format(package),
-                      'a', encoding='utf-8') as f:
-                f.write(self.command + '\n')
+            with open(
+                "debian/{}.debhelper.log".format(package), "a", encoding="utf-8"
+            ) as f:
+                f.write(self.command + "\n")
 
-    def save(self):
+    def save(self) -> None:
         self.save_substvars()
         self.save_autoscripts()
         self.save_rtupdate()
diff -pruN 6.20251204.1/dhpython/depends.py 6.20251221/dhpython/depends.py
--- 6.20251204.1/dhpython/depends.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/depends.py	2025-12-21 21:07:09.000000000 +0000
@@ -19,33 +19,65 @@
 # THE SOFTWARE.
 
 import logging
+from argparse import Namespace
 from functools import partial
 from os.path import exists, join
+
 from dhpython import PKG_PREFIX_MAP, MINPYCDEP
-from dhpython.pydist import parse_pydep, parse_requires_dist, guess_dependency
-from dhpython.version import default, supported, VersionRange
+from dhpython.fs import ScanResult
+from dhpython.debhelper import BD, DebHelper
+from dhpython.pydist import (
+    NewDependencies,
+    guess_dependency,
+    parse_pydep,
+    parse_requires_dist,
+)
+from dhpython.version import default, supported, Version, VersionRange
 
-log = logging.getLogger('dhpython')
+log = logging.getLogger("dhpython")
 
 
 class Dependencies:
     """Store relations (dependencies, etc.) between packages."""
 
-    def __init__(self, package, impl='cpython3', bdep=None):
+    impl: str
+    package: str
+    bdep: BD | None
+    is_debug_package: bool
+    ipkg_vtpl: str
+    ipkg_tpl: str
+    ipkg_tpl_ma: str
+    ipkg_vtpl_ma: str
+    python_dv_in_bd: bool
+    depends: set[str]
+    recommends: list[str]
+    suggests: list[str]
+    enhances: list[str]
+    breaks: list[str]
+    rtscripts: list[tuple[str, str]]
+
+    def __init__(
+        self,
+        package: str,
+        impl: str = "cpython3",
+        bdep: BD | None = None,
+    ) -> None:
         self.impl = impl
         self.package = package
         bdep = self.bdep = bdep or {}
-        self.is_debug_package = dbgpkg = package.endswith('-dbg')
+        self.is_debug_package = dbgpkg = package.endswith("-dbg")
 
-        self.ipkg_vtpl = 'python%s-dbg' if dbgpkg else 'python%s'
-        self.ipkg_tpl = 'python3-dbg' if dbgpkg else 'python3'
-        self.ipkg_tpl_ma = self.ipkg_tpl + ':any'
-        self.ipkg_vtpl_ma = self.ipkg_vtpl + ':any'
-
-        self.python_dev_in_bd = 'python-dev' in bdep or\
-                                'python-all-dev' in bdep or\
-                                'python3-dev' in bdep or\
-                                'python3-all-dev' in bdep
+        self.ipkg_vtpl = "python%s-dbg" if dbgpkg else "python%s"
+        self.ipkg_tpl = "python3-dbg" if dbgpkg else "python3"
+        self.ipkg_tpl_ma = self.ipkg_tpl + ":any"
+        self.ipkg_vtpl_ma = self.ipkg_vtpl + ":any"
+
+        self.python_dev_in_bd = (
+            "python-dev" in bdep
+            or "python-all-dev" in bdep
+            or "python3-dev" in bdep
+            or "python3-all-dev" in bdep
+        )
 
         self.depends = set()
         self.recommends = []
@@ -54,63 +86,83 @@ class Dependencies:
         self.breaks = []
         self.rtscripts = []
 
-    def export_to(self, dh):
+    def export_to(self, dh: DebHelper) -> None:
         """Fill in debhelper's substvars."""
-        prefix = PKG_PREFIX_MAP.get(self.impl, 'misc')
+        prefix = PKG_PREFIX_MAP.get(self.impl, "misc")
         for i in sorted(self.depends):
-            dh.addsubstvar(self.package, '{}:Depends'.format(prefix), i)
+            dh.addsubstvar(self.package, "{}:Depends".format(prefix), i)
         for i in sorted(self.recommends):
-            dh.addsubstvar(self.package, '{}:Recommends'.format(prefix), i)
+            dh.addsubstvar(self.package, "{}:Recommends".format(prefix), i)
         for i in sorted(self.suggests):
-            dh.addsubstvar(self.package, '{}:Suggests'.format(prefix), i)
+            dh.addsubstvar(self.package, "{}:Suggests".format(prefix), i)
         for i in sorted(self.enhances):
-            dh.addsubstvar(self.package, '{}:Enhances'.format(prefix), i)
+            dh.addsubstvar(self.package, "{}:Enhances".format(prefix), i)
         for i in sorted(self.breaks):
-            dh.addsubstvar(self.package, '{}:Breaks'.format(prefix), i)
-        for i in sorted(self.rtscripts):
-            dh.add_rtupdate(self.package, i)
-
-    def __str__(self):
-        return "D=%s; R=%s; S=%s; E=%s, B=%s; RT=%s" %\
-            (self.depends, self.recommends, self.suggests,
-             self.enhances, self.breaks, self.rtscripts)
+            dh.addsubstvar(self.package, "{}:Breaks".format(prefix), i)
+        for script in sorted(self.rtscripts):
+            dh.add_rtupdate(self.package, script)
+
+    def __str__(self) -> str:
+        return "D=%s; R=%s; S=%s; E=%s, B=%s; RT=%s" % (
+            self.depends,
+            self.recommends,
+            self.suggests,
+            self.enhances,
+            self.breaks,
+            self.rtscripts,
+        )
 
-    def depend(self, value):
+    def depend(self, value: str | None) -> None:
         if value and value not in self.depends:
             self.depends.add(value)
 
-    def recommend(self, value):
+    def recommend(self, value: str | None) -> None:
         if value and value not in self.recommends:
             self.recommends.append(value)
 
-    def suggest(self, value):
+    def suggest(self, value: str | None) -> None:
         if value and value not in self.suggests:
             self.suggests.append(value)
 
-    def enhance(self, value):
+    def enhance(self, value: str | None) -> None:
         if value and value not in self.enhances:
             self.enhances.append(value)
 
-    def break_(self, value):
+    def break_(self, value: str | None) -> None:
         if value and value not in self.breaks:
             self.breaks.append(value)
 
-    def rtscript(self, value):
+    def rtscript(self, value: tuple[str, str]) -> None:
         if value not in self.rtscripts:
             self.rtscripts.append(value)
 
-    def parse(self, stats, options):
-        log.debug('generating dependencies for package %s', self.package)
+    def add_new_dependencies(self, new_dependencies: NewDependencies) -> None:
+        for dep in new_dependencies["depends"]:
+            self.depend(dep)
+        for dep in new_dependencies["recommends"]:
+            self.recommend(dep)
+        for dep in new_dependencies["suggests"]:
+            self.suggest(dep)
+
+    def parse(self, stats: ScanResult, options: Namespace) -> None:
+        log.debug("generating dependencies for package %s", self.package)
         tpl = self.ipkg_tpl
         vtpl = self.ipkg_vtpl
         tpl_ma = self.ipkg_tpl_ma
         vtpl_ma = self.ipkg_vtpl_ma
         vrange = options.vrange
 
-        if vrange and any((stats['compile'], stats['public_vers'],
-                           stats['ext_vers'], stats['ext_no_version'],
-                           stats['ext_stableabi'], stats['shebangs'])):
-            if any((stats['compile'], stats['public_vers'], stats['shebangs'])):
+        if vrange and any(
+            (
+                stats["compile"],
+                stats["public_vers"],
+                stats["ext_vers"],
+                stats["ext_no_version"],
+                stats["ext_stableabi"],
+                stats["shebangs"],
+            )
+        ):
+            if any((stats["compile"], stats["public_vers"], stats["shebangs"])):
                 tpl_tmp = tpl_ma
             else:
                 tpl_tmp = tpl
@@ -125,61 +177,74 @@ class Dependencies:
             if maxv:
                 self.depend("%s (<< %s)" % (tpl_tmp, maxv))
 
-        if stats['ext_vers']:
-            sorted_vers = sorted(stats['ext_vers'])
+        if stats["ext_vers"]:
+            sorted_vers = sorted(stats["ext_vers"])
             minv = sorted_vers[0]
             maxv = sorted_vers[-1]
-            #self.depend('|'.join(vtpl % i for i in stats['ext_vers']))
+            # self.depend('|'.join(vtpl % i for i in stats['ext_vers']))
             self.depend("%s (>= %s~)" % (tpl, minv))
             self.depend("%s (<< %s)" % (tpl, maxv + 1))
-        elif stats['ext_stableabi']:
+        elif stats["ext_stableabi"]:
             self.depend("%s" % tpl)
 
         # make sure py{,3}compile binary is available
-        if stats['compile'] and self.impl in MINPYCDEP:
+        if stats["compile"] and self.impl in MINPYCDEP:
             self.depend(MINPYCDEP[self.impl])
 
-        for ipreter in stats['shebangs']:
+        for ipreter in stats["shebangs"]:
             self.depend("%s:any" % ipreter)
 
         supported_versions = supported(self.impl)
         default_version = default(self.impl)
-        for private_dir, details in stats['private_dirs'].items():
-            versions = list(i.version for i in details.get('shebangs', []) if i.version and i.version.minor)
+        for private_dir, details in stats["private_dirs"].items():
+            versions = list(
+                i.version
+                for i in details.get("shebangs", [])
+                if i.version and i.version.minor
+            )
 
             for v in versions:
                 if v in supported_versions:
                     self.depend(vtpl_ma % v)
                 else:
-                    log.info('dependency on %s (from shebang) ignored'
-                             ' - it\'s not supported anymore', vtpl % v)
+                    log.info(
+                        "dependency on %s (from shebang) ignored"
+                        " - it's not supported anymore",
+                        vtpl % v,
+                    )
             # /usr/bin/python{,3} shebang → add python{,3} to Depends
-            if any(True for i in details.get('shebangs', []) if i.version is None or i.version.minor is None):
+            if any(
+                True
+                for i in details.get("shebangs", [])
+                if i.version is None or i.version.minor is None
+            ):
                 self.depend(tpl_ma)
 
-            extensions = False
+            extensions: list[Version] = []
             if self.python_dev_in_bd:
-                extensions = sorted(details.get('ext_vers', set()))
-                #self.depend('|'.join(vtpl % i for i in extensions))
+                extensions = sorted(details.get("ext_vers", set()))
+                # self.depend('|'.join(vtpl % i for i in extensions))
                 if extensions:
                     self.depend("%s (>= %s~)" % (tpl, extensions[0]))
                     self.depend("%s (<< %s)" % (tpl, extensions[-1] + 1))
-                elif details.get('ext_no_version'):
+                elif details.get("ext_no_version"):
                     # assume unrecognized extension was built for default interpreter version
                     self.depend("%s (>= %s~)" % (tpl, default_version))
                     self.depend("%s (<< %s)" % (tpl, default_version + 1))
 
-            if details.get('compile'):
+            if details.get("compile"):
                 if self.impl in MINPYCDEP:
                     self.depend(MINPYCDEP[self.impl])
-                args = ''
+                args = ""
                 if extensions:
-                    args += "-V %s" % VersionRange(minver=extensions[0], maxver=extensions[-1])
+                    args += "-V %s" % VersionRange(
+                        minver=extensions[0], maxver=extensions[-1]
+                    )
                 elif len(versions) == 1:  # only one version from shebang
-                    #if versions[0] in supported_versions:
+                    # if versions[0] in supported_versions:
                     args += "-V %s" % versions[0]
                     # ... otherwise compile with default version
-                elif details.get('ext_no_version'):
+                elif details.get("ext_no_version"):
                     # assume unrecognized extension was built for default interpreter version
                     args += "-V %s" % default_version
                 elif vrange:
@@ -197,34 +262,35 @@ class Dependencies:
                 self.rtscript((private_dir, args))
 
         section_options = {
-            'depends_sec': options.depends_section,
-            'recommends_sec': options.recommends_section,
-            'suggests_sec': options.suggests_section,
+            "depends_sec": options.depends_section,
+            "recommends_sec": options.recommends_section,
+            "suggests_sec": options.suggests_section,
         }
-        guess_deps = partial(guess_dependency, impl=self.impl, bdep=self.bdep,
-                             accept_upstream_versions=options.accept_upstream_versions)
+        guess_deps = partial(
+            guess_dependency,
+            impl=self.impl,
+            bdep=self.bdep,
+            accept_upstream_versions=options.accept_upstream_versions,
+        )
         if options.guess_deps:
-            for fn in stats['requires.txt']:
+            for fn in stats["requires.txt"]:
                 # TODO: should options.recommends and options.suggests be
                 # removed from requires.txt?
-                deps = parse_pydep(self.impl, fn, bdep=self.bdep, **section_options)
-                # pylint: disable=expression-not-assigned
-                [self.depend(i) for i in deps['depends']]
-                [self.recommend(i) for i in deps['recommends']]
-                [self.suggest(i) for i in deps['suggests']]
-            for fpath in stats['egg-info']:
-                with open(fpath, 'r', encoding='utf-8') as fp:
+                self.add_new_dependencies(
+                    parse_pydep(self.impl, fn, bdep=self.bdep, **section_options)
+                )
+            for fpath in stats["egg-info"]:
+                with open(fpath, "r", encoding="utf-8") as fp:
                     for line in fp:
-                        if line.startswith('Requires: '):
+                        if line.startswith("Requires: "):
                             req = line[10:].strip()
                             self.depend(guess_deps(req=req))
-            for fpath in stats['dist-info']:
-                deps = parse_requires_dist(self.impl, fpath, bdep=self.bdep,
-                                           **section_options)
-                # pylint: disable=expression-not-assigned
-                [self.depend(i) for i in deps['depends']]
-                [self.recommend(i) for i in deps['recommends']]
-                [self.suggest(i) for i in deps['suggests']]
+            for fpath in stats["dist-info"]:
+                self.add_new_dependencies(
+                    parse_requires_dist(
+                        self.impl, fpath, bdep=self.bdep, **section_options
+                    )
+                )
 
         # add dependencies from --depends
         for item in options.depends or []:
@@ -237,16 +303,14 @@ class Dependencies:
             self.suggest(guess_deps(req=item))
         # add dependencies from --requires
         for fn in options.requires or []:
-            fpath = join('debian', self.package, fn)
+            fpath = join("debian", self.package, fn)
             if not exists(fpath):
                 fpath = fn
                 if not exists(fpath):
-                    log.warning('cannot find requirements file: %s', fn)
+                    log.warning("cannot find requirements file: %s", fn)
                     continue
-            deps = parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options)
-            # pylint: disable=expression-not-assigned
-            [self.depend(i) for i in deps['depends']]
-            [self.recommend(i) for i in deps['recommends']]
-            [self.suggest(i) for i in deps['suggests']]
+            self.add_new_dependencies(
+                parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options)
+            )
 
         log.debug(self)
diff -pruN 6.20251204.1/dhpython/fs.py 6.20251221/dhpython/fs.py
--- 6.20251204.1/dhpython/fs.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/fs.py	2025-12-21 21:07:09.000000000 +0000
@@ -23,20 +23,28 @@ import logging
 import os
 import re
 import sys
+from argparse import Namespace
 from filecmp import cmp as cmpfile
-from os.path import (lexists, exists, isdir, islink, join, realpath, split,
-                     splitext)
+from os.path import lexists, exists, isdir, islink, join, realpath, split, splitext
+from pathlib import Path
 from shutil import rmtree
 from stat import ST_MODE, S_IXUSR, S_IXGRP, S_IXOTH
+from typing import Iterable, Literal, Sequence, TypedDict
+
 from dhpython import MULTIARCH_DIR_TPL
 from dhpython.tools import fix_shebang, clean_egg_name
 from dhpython.interpreter import EXTFILE_RE, Interpreter
 from dhpython.version import Version
 
-log = logging.getLogger('dhpython')
+log = logging.getLogger("dhpython")
 
 
-def fix_locations(package, interpreter, versions, options):
+def fix_locations(
+    package: str,
+    interpreter: Interpreter,
+    versions: Sequence[Version],
+    options: Namespace,
+) -> None:
     """Move files to the right location."""
     # make a copy since we change version later
     interpreter = Interpreter(interpreter)
@@ -48,7 +56,7 @@ def fix_locations(package, interpreter,
         for srcdir in interpreter.old_sitedirs(package):
             if isdir(srcdir):
                 # TODO: what about relative symlinks?
-                log.debug('moving files from %s to %s', srcdir, dstdir)
+                log.debug("moving files from %s to %s", srcdir, dstdir)
                 share_files(srcdir, dstdir, interpreter, options)
                 try:
                     os.removedirs(srcdir)
@@ -59,7 +67,7 @@ def fix_locations(package, interpreter,
         dstdir = interpreter.sitedir(package, gdb=True)
         for srcdir in interpreter.old_sitedirs(package, gdb=True):
             if isdir(srcdir):
-                log.debug('moving files from %s to %s', srcdir, dstdir)
+                log.debug("moving files from %s to %s", srcdir, dstdir)
                 share_files(srcdir, dstdir, interpreter, options)
                 try:
                     os.removedirs(srcdir)
@@ -67,19 +75,24 @@ def fix_locations(package, interpreter,
                     pass
 
 
-def share_files(srcdir, dstdir, interpreter, options):
+def share_files(
+    srcdir: str,
+    dstdir: str,
+    interpreter: Interpreter,
+    options: Namespace,
+) -> None:
     """Try to move as many files from srcdir to dstdir as possible."""
     for i in os.listdir(srcdir):
         fpath1 = join(srcdir, i)
         if not lexists(fpath1):  # removed in rename_ext
             continue
-        if i.endswith('.pyc'):  # f.e. when tests were invoked on installed files
+        if i.endswith(".pyc"):  # f.e. when tests were invoked on installed files
             os.remove(fpath1)
             continue
         if i == ".coverage":  # from running tests with coverage
             os.remove(fpath1)
             continue
-        if not options.no_ext_rename and splitext(i)[-1] == '.so':
+        if not options.no_ext_rename and splitext(i)[-1] == ".so":
             # try to rename extension here as well (in :meth:`scan` info about
             # Python version is gone)
             version = interpreter.parse_public_dir(srcdir)
@@ -87,8 +100,9 @@ def share_files(srcdir, dstdir, interpre
                 fpath1 = Scan.rename_ext(fpath1, interpreter, version)
                 i = split(fpath1)[-1]
         if srcdir.endswith(".dist-info"):
-            if (i.startswith(('LICENCE', 'LICENSE', 'COPYING', 'NOTICE', 'AUTHORS'))
-                    or i in ('licenses', 'license_files')):
+            if i.startswith(
+                ("LICENCE", "LICENSE", "COPYING", "NOTICE", "AUTHORS")
+            ) or i in ("licenses", "license_files"):
                 if isdir(fpath1):
                     rmtree(fpath1)
                 else:
@@ -110,8 +124,10 @@ def share_files(srcdir, dstdir, interpre
             share_files(fpath1, fpath2, interpreter, options)
         elif cmpfile(fpath1, fpath2, shallow=False):
             os.remove(fpath1)
-        elif ext_m and ext_m.group("stableabi") and interpreter.parse_public_dir(srcdir):
-            log.warning('%s differs from previous one, removing anyway (%s)', i, srcdir)
+        elif (
+            ext_m and ext_m.group("stableabi") and interpreter.parse_public_dir(srcdir)
+        ):
+            log.warning("%s differs from previous one, removing anyway (%s)", i, srcdir)
             os.remove(fpath1)
         elif srcdir.endswith(".dist-info"):
             # dist-info file that differs... try merging
@@ -123,8 +139,8 @@ def share_files(srcdir, dstdir, interpre
                 log.warning("No merge driver for dist-info file %s", i)
         else:
             # The files differed so we cannot collapse them.
-            log.warning('Paths differ: %s and %s', fpath1, fpath2)
-            if options.verbose and not i.endswith(('.so', '.a')):
+            log.warning("Paths differ: %s and %s", fpath1, fpath2)
+            if options.verbose and not i.endswith((".so", ".a")):
                 with open(fpath1, encoding="UTF-8") as fp1:
                     fromlines = fp1.readlines()
                 with open(fpath2, encoding="UTF-8") as fp2:
@@ -140,7 +156,8 @@ def share_files(srcdir, dstdir, interpre
 
 ## Functions to merge parts of the .dist-info metadata directory together
 
-def missing_lines(src, dst):
+
+def missing_lines(src: str | Path, dst: str | Path) -> list[str]:
     """Find all the lines in the text file src that are not in dst"""
     with open(dst, encoding="UTF-8") as fh:
         current = {k: None for k in fh.readlines()}
@@ -154,7 +171,7 @@ def missing_lines(src, dst):
     return missing
 
 
-def merge_WHEEL(src, dst):
+def merge_WHEEL(src: str | Path, dst: str | Path) -> int:
     """Merge the source .dist-info/WHEEL file into the destination
 
     Note that after editing the WHEEL file, the sha256 included in
@@ -173,7 +190,7 @@ def merge_WHEEL(src, dst):
     return len(missing)
 
 
-def write_INSTALLER(distdir):
+def write_INSTALLER(distdir: str) -> None:
     """Write 'debian' as the INSTALLER"""
     log.debug("Writing INSTALLER in %s", distdir)
     installer = join(distdir, "INSTALLER")
@@ -181,7 +198,7 @@ def write_INSTALLER(distdir):
         f.write("debian\n")
 
 
-def remove_RECORD(distdir):
+def remove_RECORD(distdir: str) -> None:
     """Remove RECORD from a .dist-info.
 
     Having this breaks multi-arch installation, as the contents differ.
@@ -193,11 +210,44 @@ def remove_RECORD(distdir):
         pass
 
 
+ScanResult = TypedDict(
+    "ScanResult",
+    {
+        "requires.txt": set[str],
+        "egg-info": set[str],
+        "dist-info": set[str],
+        "nsp.txt": set[str],
+        "shebangs": set[Interpreter],
+        "public_vers": set[Version],
+        "private_dirs": dict[str, "ScanResult"],
+        "compile": bool,
+        "ext_vers": set[Version],
+        "ext_stableabi": set[str],
+        "ext_no_version": set[str],
+    },
+)
+
+
 class Scan:
-    UNWANTED_DIRS = re.compile(r'.*/__pycache__(/.*)?$')
-    UNWANTED_FILES = re.compile(r'.*\.py[co]$')
+    UNWANTED_DIRS = re.compile(r".*/__pycache__(/.*)?$")
+    UNWANTED_FILES = re.compile(r".*\.py[co]$")
 
-    def __init__(self, interpreter, package, dpath=None, options=None):
+    interprter: Interpreter
+    impl: str
+    package: str
+    proot: str
+    dpath: str | None | Literal[False]
+    options: Namespace
+    result: ScanResult
+
+    def __init__(
+        self,
+        interpreter: Interpreter,
+        package: str,
+        dpath: str | None = None,
+        *,
+        options: Namespace,
+    ) -> None:
         self.interpreter = interpreter
         self.impl = interpreter.impl
 
@@ -206,23 +256,25 @@ class Scan:
         if not dpath:
             self.proot = "debian/%s" % self.package
         else:
-            dpath = dpath.strip('/')
-            self.proot = join('debian', self.package, dpath)
+            dpath = dpath.strip("/")
+            self.proot = join("debian", self.package, dpath)
         self.dpath = dpath
         del dpath
 
         self.options = options
-        self.result = {'requires.txt': set(),
-                       'egg-info': set(),
-                       'dist-info': set(),
-                       'nsp.txt': set(),
-                       'shebangs': set(),
-                       'public_vers': set(),
-                       'private_dirs': {},
-                       'compile': False,
-                       'ext_vers': set(),
-                       'ext_stableabi': set(),
-                       'ext_no_version': set()}
+        self.result = {
+            "requires.txt": set(),
+            "egg-info": set(),
+            "dist-info": set(),
+            "nsp.txt": set(),
+            "shebangs": set(),
+            "public_vers": set(),
+            "private_dirs": {},
+            "compile": False,
+            "ext_vers": set(),
+            "ext_stableabi": set(),
+            "ext_no_version": set(),
+        }
 
         for root, dirs, file_names in os.walk(self.proot):
             if interpreter.should_ignore(root):
@@ -241,17 +293,18 @@ class Scan:
                 self.current_dir_is_public = False
 
             if self.current_dir_is_public:
-                if root.endswith('-packages'):
+                if root.endswith("-packages"):
                     if version is not None:
-                        self.result['public_vers'].add(version)
+                        assert isinstance(version, Version)
+                        self.result["public_vers"].add(version)
                     for name in dirs[:]:
-                        if name in ('test', 'tests') or name.startswith('.'):
-                            log.debug('removing dist-packages/%s', name)
+                        if name in ("test", "tests") or name.startswith("."):
+                            log.debug("removing dist-packages/%s", name)
                             rmtree(join(root, name))
                             dirs.remove(name)
                     for name in file_names[:]:
-                        if name.startswith('.'):
-                            log.debug('removing dist-packages/%s', name)
+                        if name.startswith("."):
+                            log.debug("removing dist-packages/%s", name)
                             os.remove(join(root, name))
                             file_names.remove(name)
             else:
@@ -284,7 +337,7 @@ class Scan:
                 fpath = join(root, fn)
 
                 if self.is_unwanted_file(fpath):
-                    log.debug('removing unwanted: %s', fpath)
+                    log.debug("removing unwanted: %s", fpath)
                     os.remove(fpath)
                     continue
 
@@ -294,10 +347,10 @@ class Scan:
 
                 if not exists(fpath):
                     # possibly removed while handling .so symlinks
-                    if islink(fpath) and '.so.' in split(fpath)[-1]:
+                    if islink(fpath) and ".so." in split(fpath)[-1]:
                         # dangling symlink to (now removed/renamed) .so file
                         # which wasn't removed yet (see test203's quux.so.0)
-                        log.info('removing dangling symlink: %s', fpath)
+                        log.info("removing dangling symlink: %s", fpath)
                         os.remove(fpath)
                     continue
 
@@ -307,6 +360,7 @@ class Scan:
                     if not self.options.no_ext_rename:
                         fpath = self.rename_ext(fpath, interpreter, version)
                         ext_m = EXTFILE_RE.search(os.path.basename(fpath))
+                        assert ext_m is not None
                     ver = version
 
                     if ext_m["ver"]:
@@ -317,28 +371,35 @@ class Scan:
                     else:
                         ver = None
                     if ver:
-                        self.current_result.setdefault('ext_vers', set()).add(ver)
+                        self.current_result.setdefault("ext_vers", set()).add(ver)
                     elif ext_m["stableabi"]:
-                        self.current_result.setdefault('ext_stableabi', set()).add(fpath)
+                        self.current_result.setdefault("ext_stableabi", set()).add(
+                            fpath
+                        )
                     else:
-                        self.current_result.setdefault('ext_no_version', set()).add(fpath)
+                        self.current_result.setdefault("ext_no_version", set()).add(
+                            fpath
+                        )
 
                 if self.current_private_dir:
-                    if exists(fpath) and fext != 'so':
+                    if exists(fpath) and fext != "so":
                         mode = os.stat(fpath)[ST_MODE]
                         if mode & S_IXUSR or mode & S_IXGRP or mode & S_IXOTH:
-                            if (options.no_shebang_rewrite or
-                                fix_shebang(fpath, self.options.shebang)) and \
-                                    not self.options.ignore_shebangs:
+                            if (
+                                options.no_shebang_rewrite
+                                or fix_shebang(fpath, self.options.shebang)
+                            ) and not self.options.ignore_shebangs:
                                 try:
                                     res = Interpreter.from_file(fpath)
                                 except Exception as e:
-                                    log.debug('cannot parse shebang %s: %s', fpath, e)
+                                    log.debug("cannot parse shebang %s: %s", fpath, e)
                                 else:
-                                    self.current_result.setdefault('shebangs', set()).add(res)
+                                    self.current_result.setdefault(
+                                        "shebangs", set()
+                                    ).add(res)
 
-                if fext == 'py':
-                    self.current_result['compile'] = True
+                if fext == "py":
+                    self.current_result["compile"] = True
 
             if not dirs and not self.current_private_dir:
                 try:
@@ -349,24 +410,43 @@ class Scan:
         log.debug("package %s details = %s", package, self.result)
 
     @property
-    def current_result(self):
+    def current_result(self) -> ScanResult:
         if self.current_private_dir:
-            return self.result['private_dirs'].setdefault(self.current_private_dir, {})
+            return self.result["private_dirs"].setdefault(
+                self.current_private_dir,
+                {
+                    "requires.txt": set(),
+                    "egg-info": set(),
+                    "dist-info": set(),
+                    "nsp.txt": set(),
+                    "shebangs": set(),
+                    "public_vers": set(),
+                    "private_dirs": {},
+                    "compile": False,
+                    "ext_vers": set(),
+                    "ext_stableabi": set(),
+                    "ext_no_version": set(),
+                },
+            )
         return self.result
 
-    def is_unwanted_dir(self, dpath):
-        return self.__class__.UNWANTED_DIRS.match(dpath)
+    def is_unwanted_dir(self, dpath: str) -> bool:
+        return self.__class__.UNWANTED_DIRS.match(dpath) is not None
 
-    def is_unwanted_file(self, fpath):
+    def is_unwanted_file(self, fpath: str) -> bool:
         if self.__class__.UNWANTED_FILES.match(fpath):
             return True
-        if self.current_dir_is_public and self.is_dbg_package\
-                and self.options.clean_dbg_pkg\
-                and splitext(fpath)[-1][1:] not in ('so', 'h'):
+        if (
+            self.current_dir_is_public
+            and self.is_dbg_package
+            and self.options.clean_dbg_pkg
+            and splitext(fpath)[-1][1:] not in ("so", "h")
+        ):
             return True
+        return False
 
     @property
-    def private_dirs_to_check(self):
+    def private_dirs_to_check(self) -> list[str]:
         if self.dpath:
             # scan private directory *only*
             return [self.dpath]
@@ -374,26 +454,33 @@ class Scan:
         if self.dpath is False:
             result = []
         else:
-            result = [i % self.package for i in (
-                      'usr/lib/%s',
-                      'usr/lib/games/%s',
-                      'usr/share/%s',
-                      'usr/share/games/%s')]
+            result = [
+                i % self.package
+                for i in (
+                    "usr/lib/%s",
+                    "usr/lib/games/%s",
+                    "usr/share/%s",
+                    "usr/share/games/%s",
+                )
+            ]
         return result
 
     @property
-    def is_dbg_package(self):
-        #return self.interpreter.debug
-        return self.package.endswith('-dbg')
+    def is_dbg_package(self) -> bool:
+        # return self.interpreter.debug
+        return self.package.endswith("-dbg")
 
-    def check_private_dir(self, dpath):
+    def check_private_dir(self, dpath: str) -> str | None:
         """Return private dir's root if it's a private dir."""
         for i in self.private_dirs_to_check:
-            if dpath.startswith(join('debian', self.package, i)):
-                return '/' + i
+            if dpath.startswith(join("debian", self.package, i)):
+                return "/" + i
+        return None
 
     @staticmethod
-    def rename_ext(fpath, interpreter, current_pub_version=None):
+    def rename_ext(
+        fpath: str, interpreter: Interpreter, current_pub_version: Version | None = None
+    ) -> str:
         """Add multiarch triplet, etc. Return new name.
 
         This method is invoked for all .so files in public or private directories.
@@ -401,7 +488,7 @@ class Scan:
         # current_pub_version - version parsed from dist-packages (True if unversioned)
         # i.e. if it's not None - it's a public dist-packages directory
 
-        path, fname = fpath.rsplit('/', 1)
+        path, fname = fpath.rsplit("/", 1)
         if current_pub_version is not None and islink(fpath):
             # replace symlinks with extensions in dist-packages directory
             dstfpath = fpath
@@ -409,12 +496,12 @@ class Scan:
             while islink(dstfpath):
                 links.add(dstfpath)
                 dstfpath = join(path, os.readlink(dstfpath))
-            if exists(dstfpath) and '.so.' in split(dstfpath)[-1]:
+            if exists(dstfpath) and ".so." in split(dstfpath)[-1]:
                 # rename .so.$FOO symlinks, remove other ones
                 for lpath in links:
-                    log.info('removing symlink: %s', lpath)
+                    log.info("removing symlink: %s", lpath)
                     os.remove(lpath)
-                log.info('renaming %s to %s', dstfpath, fname)
+                log.info("renaming %s to %s", dstfpath, fname)
                 os.rename(dstfpath, fpath)
 
         if MULTIARCH_DIR_TPL.match(fpath):
@@ -426,25 +513,27 @@ class Scan:
             # TODO: what about symlinks pointing to this file
             new_fpath = join(path, new_fn)
             if exists(new_fpath):
-                log.warning('destination file exist, '
-                         'cannot rename %s to %s', fname, new_fn)
+                log.warning(
+                    "destination file exist, cannot rename %s to %s", fname, new_fn
+                )
             else:
-                log.info('renaming %s to %s', fname, new_fn)
+                log.info("renaming %s to %s", fname, new_fn)
                 os.rename(fpath, new_fpath)
             return new_fpath
         return fpath
 
-    def is_bin_dir(self, dpath):
+    def is_bin_dir(self, dpath: str) -> bool:
         """Check if dir is one from PATH ones."""
         # dname = debian/packagename/usr/games
-        spath = dpath.strip('/').split('/', 4)
+        spath = dpath.strip("/").split("/", 4)
         if len(spath) > 4:
             return False  # assume bin directories don't have subdirectories
-        if dpath.endswith(('/sbin', '/bin', '/usr/games')):
+        if dpath.endswith(("/sbin", "/bin", "/usr/games")):
             # /(s)bin or /usr/(s)bin or /usr/games
             return True
+        return False
 
-    def handle_bin_dir(self, dpath, file_names):
+    def handle_bin_dir(self, dpath: str, file_names: Iterable[str]) -> None:
         if self.options.no_shebang_rewrite or self.options.ignore_shebangs:
             return
         for fn in file_names:
@@ -453,16 +542,17 @@ class Scan:
                 try:
                     res = Interpreter.from_file(fpath)
                 except Exception as e:
-                    log.debug('cannot parse shebang %s: %s', fpath, e)
+                    log.debug("cannot parse shebang %s: %s", fpath, e)
                 else:
-                    self.result['shebangs'].add(res)
+                    self.result["shebangs"].add(res)
 
-    def is_egg_dir(self, dname):
+    def is_egg_dir(self, dname: str) -> bool:
         """Check if given directory contains egg-info."""
-        return dname.endswith('.egg-info')
+        return dname.endswith(".egg-info")
 
-    def handle_egg_dir(self, dpath, file_names):
-        path, dname = dpath.rsplit('/', 1)
+    def handle_egg_dir(self, dpath: str, file_names: list[str]) -> None:
+        # NOTE: Mutates file_names
+        path, dname = dpath.rsplit("/", 1)
         if self.is_dbg_package and self.options.clean_dbg_pkg:
             rmtree(dpath)
             return
@@ -470,47 +560,46 @@ class Scan:
         clean_name = clean_egg_name(dname)
         if clean_name != dname:
             if exists(join(path, clean_name)):
-                log.info('removing %s (%s is already available)', dname, clean_name)
+                log.info("removing %s (%s is already available)", dname, clean_name)
                 rmtree(dpath)
                 return
             else:
-                log.info('renaming %s to %s', dname, clean_name)
+                log.info("renaming %s to %s", dname, clean_name)
                 os.rename(dpath, join(path, clean_name))
                 dname = clean_name
                 dpath = join(path, dname)
         if file_names:
-            if 'requires.txt' in file_names:
-                self.result['requires.txt'].add(join(dpath, 'requires.txt'))
-            if 'namespace_packages.txt' in file_names:
-                self.result['nsp.txt'].add(join(dpath, 'namespace_packages.txt'))
-            if 'PKG-INFO' in file_names:
-                self.result['egg-info'].add(join(dpath, 'PKG-INFO'))
-            if 'SOURCES.txt' in file_names:
-                os.remove(join(dpath, 'SOURCES.txt'))
-                file_names.remove('SOURCES.txt')
+            if "requires.txt" in file_names:
+                self.result["requires.txt"].add(join(dpath, "requires.txt"))
+            if "namespace_packages.txt" in file_names:
+                self.result["nsp.txt"].add(join(dpath, "namespace_packages.txt"))
+            if "PKG-INFO" in file_names:
+                self.result["egg-info"].add(join(dpath, "PKG-INFO"))
+            if "SOURCES.txt" in file_names:
+                os.remove(join(dpath, "SOURCES.txt"))
+                file_names.remove("SOURCES.txt")
 
-    def is_egg_file(self, fpath):
+    def is_egg_file(self, fpath: str) -> bool:
         """Check if given file contains egg-info."""
-        return fpath.endswith('.egg-info')
+        return fpath.endswith(".egg-info")
 
-    def handle_egg_file(self, fpath):
-        root, name = fpath.rsplit('/', 1)
+    def handle_egg_file(self, fpath: str) -> None:
+        root, name = fpath.rsplit("/", 1)
         clean_name = clean_egg_name(name)
         if clean_name != name:
             if exists(join(root, clean_name)):
-                log.info('removing %s (%s is already available)',
-                         name, clean_name)
+                log.info("removing %s (%s is already available)", name, clean_name)
                 os.remove(fpath)
             else:
-                log.info('renaming %s to %s', name, clean_name)
+                log.info("renaming %s to %s", name, clean_name)
                 os.rename(fpath, join(root, clean_name))
-        self.result['egg-info'].add(join(root, clean_name))
+        self.result["egg-info"].add(join(root, clean_name))
 
-    def is_dist_dir(self, dname):
+    def is_dist_dir(self, dname: str) -> bool:
         """Check if given directory contains dist-info."""
-        return dname.endswith('.dist-info')
+        return dname.endswith(".dist-info")
 
-    def handle_dist_dir(self, dpath, file_names):
+    def handle_dist_dir(self, dpath: str, file_names: Iterable[str]) -> None:
         if self.is_dbg_package and self.options.clean_dbg_pkg:
             rmtree(dpath)
             return
@@ -518,15 +607,15 @@ class Scan:
         if file_names:
             write_INSTALLER(dpath)
             remove_RECORD(dpath)
-            if 'METADATA' in file_names:
-                self.result['dist-info'].add(join(dpath, 'METADATA'))
+            if "METADATA" in file_names:
+                self.result["dist-info"].add(join(dpath, "METADATA"))
 
-    def cleanup(self):
+    def cleanup(self) -> None:
         if self.is_dbg_package and self.options.clean_dbg_pkg:
             # remove empty directories in -dbg packages
-            proot = self.proot + '/usr/lib'
+            proot = self.proot + "/usr/lib"
             for root, _, file_names in os.walk(proot, topdown=False):
-                if '-packages/' in root and not file_names:
+                if "-packages/" in root and not file_names:
                     try:
                         os.removedirs(root)
                     except Exception:
diff -pruN 6.20251204.1/dhpython/interpreter.py 6.20251221/dhpython/interpreter.py
--- 6.20251204.1/dhpython/interpreter.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/interpreter.py	2025-12-21 21:07:09.000000000 +0000
@@ -21,10 +21,18 @@
 import logging
 import os
 import re
-from os.path import exists, join, split
+from os.path import exists, join
+from pathlib import Path
+from typing import ClassVar, NamedTuple, Self, TYPE_CHECKING
+
 from dhpython import INTERPRETER_DIR_TPLS, PUBLIC_DIR_RE, OLD_SITE_DIRS
 
-SHEBANG_RE = re.compile(r'''
+if TYPE_CHECKING:
+    from typing import Literal
+
+
+SHEBANG_RE = re.compile(
+    r"""
     (?:\#!\s*){0,1}  # shebang prefix
     (?P<path>
         .*?/bin/.*?)?
@@ -35,8 +43,11 @@ SHEBANG_RE = re.compile(r'''
     (?P<debug>
         -dbg)?
     (?P<options>.*)
-    ''', re.VERBOSE)
-EXTFILE_RE = re.compile(r'''
+    """,
+    re.VERBOSE,
+)
+EXTFILE_RE = re.compile(
+    r"""
     (?P<name>.*?)
     (?:\.
         (?:
@@ -54,8 +65,18 @@ EXTFILE_RE = re.compile(r'''
         )?
     )?
     (?P<debug>_d)?
-    \.so$''', re.VERBOSE)
-log = logging.getLogger('dhpython')
+    \.so$""",
+    re.VERBOSE,
+)
+log = logging.getLogger("dhpython")
+
+
+class Shebang(NamedTuple):
+    name: str
+    path: str | None = None
+    version: str | None = None
+    debug: bool = False
+    options: tuple[str, ...] = ()
 
 
 class Interpreter:
@@ -73,32 +94,45 @@ class Interpreter:
     :type impl: str
     :type options: tuple
     """
-    path = '/usr/bin/'
-    name = 'python'
-    version = None
-    debug = False
-    impl = ''
-    options = ()
-    _cache = {}
 
-    def __init__(self, value=None, *, path=None, name=None, version=None,
-                 debug=None, impl=None, options=None):
+    path = "/usr/bin/"
+    name = "python"
+    version: "Version | None" = None
+    debug = False
+    impl = ""
+    options: tuple[str, ...] = ()
+    _cmd_cache: ClassVar[dict[str, list[str] | str]] = {}
+    _re_cache: ClassVar[dict[str, re.Pattern[str]]] = {}
+
+    def __init__(
+        self,
+        value: Self | str | None = None,
+        *,
+        path: str | None = None,
+        name: str | None = None,
+        version: str | None = None,
+        debug: str | None = None,
+        impl: str | None = None,
+        options: str | None = None,
+    ) -> None:
         # pylint: disable=unused-argument
         params = locals()
-        del params['self']
-        del params['value']
+        del params["self"]
+        del params["value"]
 
         if isinstance(value, Interpreter):
             for key in params.keys():
                 if params[key] is None:
                     params[key] = getattr(value, key)
         elif value:
-            if value.replace('.', '').isdigit() and not version:
+            if value.replace(".", "").isdigit() and not version:
                 # version string
-                params['version'] = Version(value)
+                params["version"] = Version(value)
             else:
                 # shebang or other string
-                for key, val in self.parse(value).items():
+                shebang = self.parse(value)
+                assert shebang
+                for key, val in shebang._asdict().items():
                     # prefer values passed to constructor over shebang ones:
                     if params[key] is None:
                         params[key] = val
@@ -106,115 +140,128 @@ class Interpreter:
         for key, val in params.items():
             if val is not None:
                 setattr(self, key, val)
-            elif key == 'version':
+            elif key == "version":
                 setattr(self, key, val)
 
-    def __setattr__(self, name, value):
-        if name == 'name':
-            if value not in ('python', 'pypy', ''):
+    def __setattr__(self, name: str, value: "str | Version | None") -> None:
+        if name == "name":
+            if value not in ("python", "pypy", ""):
                 raise ValueError("interpreter not supported: %s" % value)
-            if value == 'python':
+            if value == "python":
                 if self.version:
                     if self.version.major == 3:
-                        self.__dict__['impl'] = 'cpython3'
-            elif value == 'pypy':
+                        self.__dict__["impl"] = "cpython3"
+            elif value == "pypy":
                 if self.version:
                     if self.version.major == 3:
-                        self.__dict__['impl'] = 'pypy3'
-        elif name == 'version' and value is not None:
+                        self.__dict__["impl"] = "pypy3"
+        elif name == "version" and value is not None:
             value = Version(value)
-            if not self.impl and self.name == 'python':
+            if not self.impl and self.name == "python":
                 if value.major == 3:
-                    self.impl = 'cpython3'
-        if name in ('path', 'name', 'impl', 'options') and value is None:
+                    self.impl = "cpython3"
+        if name in ("path", "name", "impl", "options") and value is None:
             pass
-        elif name == 'debug':
+        elif name == "debug":
             self.__dict__[name] = bool(value)
         else:
             self.__dict__[name] = value
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         result = self.path
-        if not result.endswith('/'):
-            result += '/'
+        if not result.endswith("/"):
+            result += "/"
         result += self._vstr(self.version)
         if self.options:
-            result += ' ' + ' '.join(self.options)
+            result += " " + " ".join(self.options)
         return result
 
-    def __str__(self):
+    def __str__(self) -> str:
         return self._vstr(self.version)
 
-    def _vstr(self, version=None, consider_default_ver=False):
-        if self.impl == 'pypy':
+    def _vstr(
+        self, version: "str | Version | None" = None, consider_default_ver: bool = False
+    ) -> str:
+        if self.impl == "pypy":
             # TODO: will Debian support more than one PyPy version?
             return self.name
-        version = version or self.version or ''
+        version = version or self.version or ""
         if consider_default_ver and (not version or version == self.default_version):
-            version = '3'
+            version = "3"
         if self.debug:
-            return 'python{}-dbg'.format(version)
+            return "python{}-dbg".format(version)
         return self.name + str(version)
 
-    def binary(self, version=None):
-        return '{}{}'.format(self.path, self._vstr(version))
+    def binary(self, version: "Version | None" = None) -> str:
+        return "{}{}".format(self.path, self._vstr(version))
 
     @property
-    def binary_dv(self):
+    def binary_dv(self) -> str:
         """Like binary(), but returns path to default intepreter symlink
         if version matches default one for given implementation.
         """
-        return '{}{}'.format(self.path, self._vstr(consider_default_ver=True))
+        return "{}{}".format(self.path, self._vstr(consider_default_ver=True))
 
     @property
-    def default_version(self):
+    def default_version(self) -> "Version | None":
         if self.impl:
             return default(self.impl)
+        return None
 
     @staticmethod
-    def parse(shebang):
+    def parse(shebang: str) -> Shebang | None:
         """Return dict with parsed shebang
 
-        >>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg').items())
-        [('debug', '-dbg'), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
-        >>> sorted(Interpreter.parse('#! /usr/bin/python3.2').items())
-        [('debug', None), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
-        >>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg --foo --bar').items())
-        [('debug', '-dbg'), ('name', 'python'), ('options', ('--foo', '--bar')),\
- ('path', '/usr/bin/'), ('version', '3.2')]
+        >>> Interpreter.parse('#!/usr/bin/python')
+        Shebang(name='python', path='/usr/bin/', version='3', debug=False, options=())
+        >>> Interpreter.parse('/usr/bin/python3.2-dbg')
+        Shebang(name='python', path='/usr/bin/', version='3.2', debug=True, options=())
+        >>> Interpreter.parse('#! /usr/bin/python3.2')
+        Shebang(name='python', path='/usr/bin/', version='3.2', debug=False, options=())
+        >>> Interpreter.parse('/usr/bin/python3.2-dbg --foo --bar')
+        Shebang(name='python', path='/usr/bin/', version='3.2', debug=True, options=('--foo', '--bar'))
         """
-        result = SHEBANG_RE.search(shebang)
-        if not result:
-            return {}
-        result = result.groupdict()
-        if 'options' in result:
+        if not (m := SHEBANG_RE.search(shebang)):
+            return None
+        groups = m.groupdict()
+        version = groups["version"]
+        if version is None and groups["name"] == "python":
+            version = "3"
+        return Shebang(
+            name=groups["name"],
+            path=groups["path"],
+            version=version,
+            debug=bool(groups["debug"]),
             # TODO: do we need "--key value" here?
-            result['options'] = tuple(result['options'].split())
-        if result['name'] == 'python' and result['version'] is None:
-            result['version'] = '3'
-        return result
+            options=tuple(groups["options"].split()),
+        )
 
     @classmethod
-    def from_file(cls, fpath):
+    def from_file(cls, fpath: str | Path) -> "Interpreter":
         """Read file's shebang and parse it."""
         interpreter = Interpreter()
-        with open(fpath, 'rb') as fp:
+        with open(fpath, "rb") as fp:
             data = fp.read(96)
             if b"\0" in data:
-                raise ValueError('cannot parse binary file')
+                raise ValueError("cannot parse binary file")
         # make sure only first line is checked
-        data = str(data, 'utf-8').split('\n', maxsplit=1)[0]
-        if not data.startswith('#!'):
-            raise ValueError("doesn't look like a shebang: %s" % data)
+        shebang = data.decode("utf-8").split("\n", maxsplit=1)[0]
+        if not shebang.startswith("#!"):
+            raise ValueError(f"doesn't look like a shebang: {shebang}")
 
-        parsed = cls.parse(data)
+        parsed = cls.parse(shebang)
         if not parsed:
-            raise ValueError("doesn't look like a shebang: %s" % data)
-        for key, val in parsed.items():
+            raise ValueError(f"doesn't look like a shebang: {shebang}")
+        for key, val in parsed._asdict().items():
             setattr(interpreter, key, val)
         return interpreter
 
-    def sitedir(self, package=None, version=None, gdb=False):
+    def sitedir(
+        self,
+        package: str | None = None,
+        version: "str | Version | None" = None,
+        gdb: bool = False,
+    ) -> str:
         """Return path to site-packages directory.
 
         Note that returned path is not the final location of .py files
@@ -231,9 +278,9 @@ class Interpreter:
             version = Version(version or self.version)
         except Exception as err:
             raise ValueError("cannot find valid version: %s" % err)
-        if version << Version('3.0'):
+        if version << Version("3.0"):
             raise ValueError(f"The version {version} is no longer supported")
-        path = '/usr/lib/python3/dist-packages/'
+        path = "/usr/lib/python3/dist-packages/"
 
         if gdb:
             path = "/usr/lib/debug%s" % path
@@ -242,7 +289,12 @@ class Interpreter:
 
         return path
 
-    def old_sitedirs(self, package=None, version=None, gdb=False):
+    def old_sitedirs(
+        self,
+        package: str | None = None,
+        version: "str | Version | None" = None,
+        gdb: bool = False,
+    ) -> list[str]:
         """Return deprecated paths to site-packages directories."""
         try:
             version = Version(version or self.version)
@@ -258,111 +310,42 @@ class Interpreter:
                     result.append(res)
 
         if gdb:
-            result = ['/usr/lib/debug{}'.format(i) for i in result]
-            if self.impl.startswith('cpython'):
-                result.append('/usr/lib/debug/usr/lib/pyshared/python{}'.format(version))
+            result = ["/usr/lib/debug{}".format(i) for i in result]
+            if self.impl.startswith("cpython"):
+                result.append(
+                    "/usr/lib/debug/usr/lib/pyshared/python{}".format(version)
+                )
         if package:
-            result = ['debian/{}{}'.format(package, i) for i in result]
+            result = ["debian/{}{}".format(package, i) for i in result]
 
         return result
 
-    def parse_public_dir(self, path):
+    def parse_public_dir(self, path: str) -> "Version | Literal[True] | None":
         """Return version assigned to site-packages path
         or True is it's unversioned public dir."""
         match = PUBLIC_DIR_RE[self.impl].match(path)
         if match:
-            vers = match.groups(0)
+            vers = match.group(1)
             if vers and vers[0]:
                 return Version(vers)
             return True
+        return None
 
-    def should_ignore(self, path):
+    def should_ignore(self, path: str) -> bool:
         """Return True if path is used by another interpreter implementation."""
         if len(INTERPRETER_DIR_TPLS) == 1:
-            return
-        cache_key = 'should_ignore_{}'.format(self.impl)
-        if cache_key not in self.__class__._cache:
+            return False
+        cache_key = f"should_ignore_{self.impl}"
+        if cache_key not in self.__class__._re_cache:
             expr = [v for k, v in INTERPRETER_DIR_TPLS.items() if k != self.impl]
-            regexp = re.compile('|'.join('({})'.format(i) for i in expr))
-            self.__class__._cache[cache_key] = regexp
+            regexp = re.compile("|".join("({})".format(i) for i in expr))
+            self.__class__._re_cache[cache_key] = regexp
         else:
-            regexp = self.__class__._cache[cache_key]
-        return regexp.search(path)
-
-    def cache_file(self, fpath, version=None):
-        """Given path to a .py file, return path to its .pyc/.pyo file.
-
-        This function is inspired by Python 3.2's imp.cache_from_source.
-
-        :param fpath: path to file name
-        :param version: Python version
-
-        >>> i = Interpreter('python')
-        >>> i.cache_file('foo.py', Version('3.1'))
-        'foo.pyc'
-        >>> i.cache_file('bar/foo.py', '3.8')          # doctest: +SKIP
-        'bar/__pycache__/foo.cpython-38.pyc'
-        """
-        version = Version(version or self.version)
-        last_char = 'o' if '-O' in self.options else 'c'
-        if version <= Version('3.1'):
-            return fpath + last_char
-
-        fdir, fname = split(fpath)
-        if not fname.endswith('.py'):
-            fname += '.py'
-        return join(fdir, '__pycache__', "%s.%s.py%s" %
-                    (fname[:-3], self.magic_tag(version), last_char))
-
-    def magic_number(self, version=None):
-        """Return magic number."""
-        version = Version(version or self.version)
-        result = self._execute('import imp; print(imp.get_magic())', version)
-        return eval(result)
-
-    def magic_tag(self, version=None):
-        """Return Python magic tag (used in __pycache__ dir to tag files).
-
-        >>> i = Interpreter('python')
-        >>> i.magic_tag(version='3.8')                 # doctest: +SKIP
-        'cpython-38'
-        """
-        version = Version(version or self.version)
-        if self.impl.startswith('cpython') and version << Version('3.2'):
-            return ''
-        return self._execute('import imp; print(imp.get_tag())', version)
-
-    def multiarch(self, version=None):
-        """Return multiarch tag."""
-        version = Version(version or self.version)
-        try:
-            _, multiarch = self._get_config(version)[:2]
-        except Exception:
-            log.debug('cannot get multiarch', exc_info=True)
-            # interpreter without multiarch support
-            return ''
-        return multiarch
-
-    def stableabi(self, version=None):
-        version = Version(version or self.version)
-        # stable ABI was introduced in Python 3.3
-        if self.impl == 'cpython3' and version >> Version('3.2'):
-            return 'abi{}'.format(version.major)
-
-    def soabi(self, version=None):
-        """Return SOABI flag (used to in .so files)."""
-        version = Version(version or self.version)
-        # NOTE: it's not the same as magic_tag
-        try:
-            soabi, _ = self._get_config(version)[:2]
-        except Exception:
-            log.debug('cannot get soabi', exc_info=True)
-            # interpreter without soabi support
-            return ''
-        return soabi
+            regexp = self.__class__._re_cache[cache_key]
+        return bool(regexp.search(path))
 
     @property
-    def include_dir(self):
+    def include_dir(self) -> str:
         """Return INCLUDE_DIR path.
 
         >>> Interpreter('python3.8').include_dir       # doctest: +SKIP
@@ -375,86 +358,88 @@ class Interpreter:
             if result:
                 return result
         except Exception:
-            result = ''
-            log.debug('cannot get include path', exc_info=True)
-        result = '/usr/include/{}'.format(self.name)
+            result = ""
+            log.debug("cannot get include path", exc_info=True)
+        result = "/usr/include/{}".format(self.name)
         version = self.version
+        assert version
         if self.debug:
-            if version >= '3.8':
-                result += 'd'
-            elif version << '3.3':
-                result += '_d'
+            if version >= "3.8":
+                result += "d"
+            elif version << "3.3":
+                result += "_d"
             else:
-                result += 'dm'
+                result += "dm"
         else:
-            if version >= '3.8':
+            if version >= "3.8":
                 pass
-            elif version >> '3.2':
-                result += 'm'
-            elif version == '3.2':
-                result += 'mu'
+            elif version >> "3.2":
+                result += "m"
+            elif version == "3.2":
+                result += "mu"
         return result
 
     @property
-    def library_file(self):
+    def library_file(self) -> str:
         """Return libfoo.so file path."""
         libpl, ldlibrary = self._get_config()[3:5]
-        if ldlibrary.endswith('.a'):
+        if ldlibrary.endswith(".a"):
             # python3.1-dbg, python3.2, python3.2-dbg returned static lib
-            ldlibrary = ldlibrary.replace('.a', '.so')
+            ldlibrary = ldlibrary.replace(".a", ".so")
         if libpl and ldlibrary:
             return join(libpl, ldlibrary)
-        raise Exception('cannot find library file for {}'.format(self))
+        raise Exception("cannot find library file for {}".format(self))
 
-    def check_extname(self, fname, version=None):
+    def check_extname(self, fname: str, version: "Version | None" = None) -> str | None:
         """Return extension file name if file can be renamed."""
         if not version and not self.version:
-            return
+            return None
 
         version = Version(version or self.version)
 
-        if '/' in fname:
-            fdir, fname = fname.rsplit('/', 1)  # in case full path was passed
+        if "/" in fname:
+            fdir, fname = fname.rsplit("/", 1)  # in case full path was passed
         else:
-            fdir = ''
+            fdir = ""
 
-        info = EXTFILE_RE.search(fname)
-        if not info:
-            return
-        info = info.groupdict()
-        if info['ver'] and (not version or version.minor is None):
+        if not (m := EXTFILE_RE.search(fname)):
+            return None
+        info = m.groupdict()
+        if info["ver"] and (not version or version.minor is None):
             # get version from soabi if version is not set of only major
             # version number is set
-            version = Version("%s.%s" % (info['ver'][0], info['ver'][1]))
+            version = Version("%s.%s" % (info["ver"][0], info["ver"][1]))
 
-        if info['stableabi'] and version < Version('3.13'):
+        if info["stableabi"] and version < Version("3.13"):
             # We added support for stableabi multiarch filenames in 3.13
             # (GH-122931)
-            return
-        if info['debug'] and self.debug is False:
+            return None
+        if info["debug"] and self.debug is False:
             # do not change Python 2.X extensions already marked as debug
             # (the other way around is acceptable)
-            return
-        if (info['soabi'] or info['stableabi']) and info['multiarch']:
+            return None
+        if (info["soabi"] or info["stableabi"]) and info["multiarch"]:
             # already tagged, nothing we can do here
-            return
+            return None
 
         try:
             soabi, multiarch = self._get_config(version)[:2]
         except Exception:
-            log.debug('cannot get soabi/multiarch', exc_info=True)
-            return
+            log.debug("cannot get soabi/multiarch", exc_info=True)
+            return None
 
-        if info['soabi'] and soabi and info['soabi'] != soabi:
-            return
+        if info["soabi"] and soabi and info["soabi"] != soabi:
+            return None
 
-        tmp_soabi = info['stableabi'] or info['soabi'] or soabi
-        tmp_multiarch = info['multiarch'] or multiarch
+        tmp_soabi = info["stableabi"] or info["soabi"] or soabi
+        tmp_multiarch = info["multiarch"] or multiarch
 
-        result = info['name']
-        if (result.endswith('module')
-                and result not in ('module', '_module')
-                and self.impl == 'cpython3'):
+        result = info["name"]
+        if (
+            result.endswith("module")
+            and result not in ("module", "_module")
+            and self.impl == "cpython3"
+        ):
             result = result[:-6]
 
         if tmp_soabi:
@@ -462,12 +447,12 @@ class Interpreter:
         if tmp_multiarch and tmp_multiarch not in result:
             result = f"{result}-{tmp_multiarch}"
 
-        result += '.so'
+        result += ".so"
         if fname == result:
-            return
+            return None
         return join(fdir, result)
 
-    def suggest_pkg_name(self, name):
+    def suggest_pkg_name(self, name: str) -> str:
         """Suggest binary package name with for given library name
 
         >>> Interpreter('python3.1').suggest_pkg_name('foo')
@@ -477,52 +462,60 @@ class Interpreter:
         >>> Interpreter('python3.8-dbg').suggest_pkg_name('bar')
         'python3-bar-dbg'
         """
-        name = name.replace('_', '-')
-        result = 'python3-{}'.format(name)
+        name = name.replace("_", "-")
+        result = "python3-{}".format(name)
         if self.debug:
-            result += '-dbg'
+            result += "-dbg"
         return result
 
-    def _get_config(self, version=None):
+    def _get_config(self, version: "str | Version | None" = None) -> list[str]:
         version = Version(version or self.version)
-        cmd = 'import sysconfig as s; print("__SEP__".join(i or "" ' \
-               'for i in s.get_config_vars('\
-               '"SOABI", "MULTIARCH", "INCLUDEPY", "LIBPL", "LDLIBRARY")))'
-        conf_vars = self._execute(cmd, version).split('__SEP__')
+        cmd = (
+            'import sysconfig as s; print("__SEP__".join(i or "" '
+            "for i in s.get_config_vars("
+            '"SOABI", "MULTIARCH", "INCLUDEPY", "LIBPL", "LDLIBRARY")))'
+        )
+        output = self._execute(cmd, version)
+        assert isinstance(output, str)
+        conf_vars = output.split("__SEP__")
         if conf_vars[1] in conf_vars[0]:
             # Python >= 3.5 includes MILTIARCH in SOABI
-            conf_vars[0] = conf_vars[0].replace("-%s" % conf_vars[1], '')
+            conf_vars[0] = conf_vars[0].replace("-%s" % conf_vars[1], "")
         try:
-            conf_vars[1] = os.environ['DEB_HOST_MULTIARCH']
+            conf_vars[1] = os.environ["DEB_HOST_MULTIARCH"]
         except KeyError:
             pass
         return conf_vars
 
-    def _execute(self, command, version=None, cache=True):
+    def _execute(
+        self, command: str, version: "str | Version | None" = None, cache: bool = True
+    ) -> list[str] | str:
         version = Version(version or self.version)
         exe = "{}{}".format(self.path, self._vstr(version))
-        command = "{} -c '{}'".format(exe, command.replace("'", "\'"))
-        if cache and command in self.__class__._cache:
-            return self.__class__._cache[command]
+        command = "{} -c '{}'".format(exe, command.replace("'", "'"))
+        if cache and command in self.__class__._cmd_cache:
+            return self.__class__._cmd_cache[command]
         if not exists(exe):
-            raise Exception("cannot execute command due to missing "
-                            "interpreter: %s" % exe)
+            raise Exception(
+                "cannot execute command due to missing " "interpreter: %s" % exe
+            )
 
         output = execute(command)
-        if output['returncode'] != 0:
-            log.debug(output['stderr'])
-            raise Exception('{} failed with status code {}'.format(command, output['returncode']))
+        if output.returncode != 0:
+            log.debug(output.stderr)
+            raise Exception(f"{command} failed with status code {output.returncode}")
 
-        result = output['stdout'].splitlines()
+        result: list[str] | str = output.stdout.splitlines()
 
         if len(result) == 1:
             result = result[0]
 
         if cache:
-            self.__class__._cache[command] = result
+            self.__class__._cmd_cache[command] = result
 
         return result
 
+
 # due to circular imports issue
 from dhpython.tools import execute
 from dhpython.version import Version, default
diff -pruN 6.20251204.1/dhpython/markers.py 6.20251221/dhpython/markers.py
--- 6.20251204.1/dhpython/markers.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/markers.py	2025-12-21 21:07:09.000000000 +0000
@@ -27,9 +27,11 @@ private: https://github.com/pypa/packagi
 """
 
 import re
+from typing import Literal, NamedTuple, cast
 
 
-SIMPLE_ENV_MARKER_RE = re.compile(r'''
+SIMPLE_ENV_MARKER_RE = re.compile(
+    r"""
     (?P<marker>[a-z_]+)
     \s*
     (?P<op><=?|>=?|[=!~]=|===)
@@ -37,22 +39,36 @@ SIMPLE_ENV_MARKER_RE = re.compile(r'''
     (?P<quote>['"])
     (?P<value>.*)  # Could contain additional markers
     (?P=quote)
-    ''', re.VERBOSE)
-COMPLEX_ENV_MARKER_RE = re.compile(r'''
+    """,
+    re.VERBOSE,
+)
+COMPLEX_ENV_MARKER_RE = re.compile(
+    r"""
     (?:\s|\))
     (?:and|or)
     (?:\s|\()
-    ''', re.VERBOSE)
+    """,
+    re.VERBOSE,
+)
 
 
 class ComplexEnvironmentMarker(Exception):
     pass
 
 
-def parse_environment_marker(marker):
+OP = Literal["<", ">", "<=", ">=", "==", "!=", "~=", "==="]
+
+
+class ParsedMarker(NamedTuple):
+    marker: str
+    op: OP
+    value: str
+
+
+def parse_environment_marker(marker: str) -> ParsedMarker:
     """Parse a simple marker of <= 1 environment restriction"""
     marker = marker.strip()
-    if marker.startswith('(') and marker.endswith(')'):
+    if marker.startswith("(") and marker.endswith(")"):
         marker = marker[1:-1].strip()
 
     m = COMPLEX_ENV_MARKER_RE.search(marker)
@@ -63,8 +79,8 @@ def parse_environment_marker(marker):
     if not m:
         raise ComplexEnvironmentMarker()
 
-    return (
-        m.group('marker'),
-        m.group('op'),
-        m.group('value'),
+    return ParsedMarker(
+        marker=m.group("marker"),
+        op=cast(OP, m.group("op")),
+        value=m.group("value"),
     )
diff -pruN 6.20251204.1/dhpython/option.py 6.20251221/dhpython/option.py
--- 6.20251204.1/dhpython/option.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/option.py	2025-12-21 21:07:09.000000000 +0000
@@ -22,7 +22,7 @@
 import re
 
 
-def compiled_regex(string):
+def compiled_regex(string: str) -> re.Pattern[str]:
     """argparse regex type"""
     try:
         return re.compile(string)
diff -pruN 6.20251204.1/dhpython/pydist.py 6.20251221/dhpython/pydist.py
--- 6.20251204.1/dhpython/pydist.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/pydist.py	2025-12-21 21:07:09.000000000 +0000
@@ -25,22 +25,32 @@ import platform
 import os
 import re
 import subprocess
-from functools import partial
+from argparse import Namespace
+from enum import Enum, StrEnum, auto
+from functools import cache, partial
 from os.path import exists, isdir, join
+from typing import Callable, Literal, NamedTuple, TypedDict, cast
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     import sys
-    sys.path.append(os.path.abspath(join(os.path.dirname(__file__), '..')))
 
-from dhpython import PKG_PREFIX_MAP, PUBLIC_DIR_RE,\
-    PYDIST_DIRS, PYDIST_OVERRIDES_FNAMES, PYDIST_DPKG_SEARCH_TPLS
+    sys.path.append(os.path.abspath(join(os.path.dirname(__file__), "..")))
+
+from dhpython import (
+    PKG_PREFIX_MAP,
+    PUBLIC_DIR_RE,
+    PYDIST_DIRS,
+    PYDIST_OVERRIDES_FNAMES,
+    PYDIST_DPKG_SEARCH_TPLS,
+)
+from dhpython.debhelper import BD
 from dhpython.markers import ComplexEnvironmentMarker, parse_environment_marker
-from dhpython.tools import memoize
 from dhpython.version import get_requested_versions, Version
 
-log = logging.getLogger('dhpython')
+log = logging.getLogger("dhpython")
 
-PYDIST_RE = re.compile(r"""
+PYDIST_RE = re.compile(
+    r"""
     (?P<name>[A-Za-z][A-Za-z0-9_.-]*)            # Python distribution name
     \s*
     (?P<vrange>(?:-?\d\.\d+(?:-(?:\d\.\d+)?)?)?) # version range
@@ -52,8 +62,11 @@ PYDIST_RE = re.compile(r"""
         \s*
         (?P<rules>(?:s|tr|y).*)?                 # translator rules
     )?
-    """, re.VERBOSE)
-REQUIRES_RE = re.compile(r'''
+    """,
+    re.VERBOSE,
+)
+REQUIRES_RE = re.compile(
+    r"""
     (?P<name>[A-Za-z][A-Za-z0-9_.-]*)    # Python distribution name
     \s*
     (?P<enabled_extras>(?:\[[^\]]*\])?)  # ignored for now
@@ -77,8 +90,11 @@ REQUIRES_RE = re.compile(r'''
     (?:;  # optional environment markers
         (?P<environment_marker>.+)
     )?
-    ''', re.VERBOSE)
-EXTRA_RE = re.compile(r'''
+    """,
+    re.VERBOSE,
+)
+EXTRA_RE = re.compile(
+    r"""
     ;
     \s*
     extra
@@ -88,8 +104,11 @@ EXTRA_RE = re.compile(r'''
     (?P<quote>['"])
     (?P<section>[a-zA-Z0-9-_.]+)
     (?P=quote)
-    ''', re.VERBOSE)
-REQ_SECTIONS_RE = re.compile(r'''
+    """,
+    re.VERBOSE,
+)
+REQ_SECTIONS_RE = re.compile(
+    r"""
     ^
     \[
     (?P<section>[a-zA-Z0-9-_.]+)?
@@ -100,189 +119,253 @@ REQ_SECTIONS_RE = re.compile(r'''
     \]
     \s*
     $
-    ''', re.VERBOSE)
+    """,
+    re.VERBOSE,
+)
 DEB_VERS_OPS = {
-    '==': '=',
-    '<':  '<<',
-    '>':  '>>',
-    '~=': '>=',
+    "==": "=",
+    "<": "<<",
+    ">": ">>",
+    "~=": ">=",
 }
 # Optimize away any dependencies on Python less than:
 MIN_PY_VERSION = [3, 11]
 
 
-def validate(fpath):
+class Standard(StrEnum):
+    PEP386 = "PEP386"
+    PEP440 = "PEP440"
+
+
+class ModificationAction(Enum):
+    SKIP = auto()
+    KEEP = auto()
+    APPEND = auto()
+    PREPEND = auto()
+
+
+class RequirementModification(NamedTuple):
+    action: ModificationAction
+    alternative: str | None = None
+
+
+def validate(fpath: str) -> bool:
     """Check if pydist file looks good."""
-    with open(fpath, encoding='utf-8') as fp:
+    with open(fpath, encoding="utf-8") as fp:
         for line in fp:
             line = line.strip()
-            if line.startswith('#') or not line:
+            if line.startswith("#") or not line:
                 continue
             if not PYDIST_RE.match(line):
-                log.error('invalid pydist data in file %s: %s',
-                          fpath.rsplit('/', 1)[-1], line)
+                log.error(
+                    "invalid pydist data in file %s: %s", fpath.rsplit("/", 1)[-1], line
+                )
                 return False
     return True
 
 
-@memoize
-def load(impl):
+class PyDist(TypedDict):
+    name: str
+    versions: set[Version]
+    dependency: str
+    standard: Standard | None
+    rules: list[str]
+
+
+@cache
+def load(impl: str) -> dict[str, list[PyDist]]:
     """Load information about installed Python distributions.
 
     :param impl: interpreter implementation, f.e. cpython3
     :type impl: str
     """
     fname = PYDIST_OVERRIDES_FNAMES.get(impl)
-    if exists(fname):
+    if fname and exists(fname):
         to_check = [fname]  # first one!
     else:
         to_check = []
 
     dname = PYDIST_DIRS.get(impl)
-    if isdir(dname):
+    if dname and isdir(dname):
         to_check.extend(join(dname, i) for i in os.listdir(dname))
 
-    fbdir = os.environ.get('DH_PYTHON_DIST', '/usr/share/dh-python/dist/')
-    fbname = join(fbdir, '{}_fallback'.format(impl))
+    fbdir = os.environ.get("DH_PYTHON_DIST", "/usr/share/dh-python/dist/")
+    fbname = join(fbdir, "{}_fallback".format(impl))
     if exists(fbname):  # fall back generated at dh-python build time
         to_check.append(fbname)  # last one!
 
-    result = {}
+    result: dict[str, list[PyDist]] = {}
     for fpath in to_check:
-        with open(fpath, encoding='utf-8') as fp:
+        assert fpath
+        with open(fpath, encoding="utf-8") as fp:
             for line in fp:
                 line = line.strip()
-                if line.startswith('#') or not line:
+                if line.startswith("#") or not line:
                     continue
-                dist = PYDIST_RE.search(line)
-                if not dist:
-                    raise Exception('invalid pydist line: %s (in %s)' % (line, fpath))
-                dist = dist.groupdict()
-                name = normalize_name(dist['name'])
-                dist['versions'] = get_requested_versions(impl, dist['vrange'])
-                dist['dependency'] = dist['dependency'].strip()
-                if dist['rules']:
-                    dist['rules'] = dist['rules'].split(';')
-                else:
-                    dist['rules'] = []
-                result.setdefault(name, []).append(dist)
+                if not (m := PYDIST_RE.search(line)):
+                    raise Exception("invalid pydist line: %s (in %s)" % (line, fpath))
+                data = m.groupdict()
+                dist = PyDist(
+                    name=normalize_name(data["name"]),
+                    versions=get_requested_versions(impl, data["vrange"]),
+                    dependency=data["dependency"].strip(),
+                    rules=data["rules"].split(";") if data["rules"] else [],
+                    standard=cast(Standard | None, data["standard"]),
+                )
+                result.setdefault(dist["name"], []).append(dist)
     return result
 
 
-def guess_dependency(impl, req, version=None, bdep=None,
-                     accept_upstream_versions=False):
+def merge_alternative_dependency(
+    first: str,
+    second: str,
+) -> str:
+    return f"{first} | {second}"
+
+
+def guess_dependency(
+    impl: str,
+    req: str,
+    version: Version | str | None = None,
+    bdep: BD | None = None,
+    accept_upstream_versions: bool = False,
+) -> str | None:
     bdep = bdep or {}
-    log.debug('trying to find dependency for %s (python=%s)',
-              req, version)
+    log.debug("trying to find dependency for %s (python=%s)", req, version)
     if isinstance(version, str):
         version = Version(version)
 
     # some upstreams have weird ideas for distribution name...
-    name, rest = re.compile(r'([^!><=~ \(\)\[;]+)(.*)').match(req).groups()
+    if not (m := re.compile(r"([^!><=~ \(\)\[;]+)(.*)").match(req)):
+        raise Exception(f"Unable to parse requirement: {req}")
+    name, rest = m.groups()
     # TODO: check stdlib and dist-packaged for name.py and name.so files
     req = normalize_name(name) + rest
 
     data = load(impl)
-    req_d = REQUIRES_RE.match(req)
-    if not req_d:
-        log.info('please ask dh_python3 author to fix REQUIRES_RE '
-                 'or your upstream author to fix requires.txt')
-        raise Exception('requirement is not valid: %s' % req)
-    req_d = req_d.groupdict()
-
-    env_marker_alts = ''
-    if req_d['environment_marker']:
-        action = check_environment_marker_restrictions(
-            req,
-            req_d['environment_marker'],
-            impl)
-        if action is False:
-            return
-        elif action is True:
-            pass
-        else:
-            env_marker_alts = ' ' + action
+    if not (m := REQUIRES_RE.match(req)):
+        log.info(
+            "please ask dh_python3 author to fix REQUIRES_RE "
+            "or your upstream author to fix requires.txt"
+        )
+        raise Exception(f"requirement is not valid: {req}")
+    req_d: dict[str, str] = m.groupdict()
+
+    merge_marker: Callable[[str], str] = str
+    if req_d["environment_marker"]:
+        modification = check_environment_marker_restrictions(
+            req, req_d["environment_marker"], impl
+        )
+        match modification.action:
+            case ModificationAction.SKIP:
+                return None
+            case ModificationAction.KEEP:
+                pass
+            case ModificationAction.APPEND:
+                assert modification.alternative
+                merge_marker = partial(
+                    merge_alternative_dependency, second=modification.alternative
+                )
+            case ModificationAction.PREPEND:
+                assert modification.alternative
+                merge_marker = partial(
+                    merge_alternative_dependency, modification.alternative
+                )
 
-    name = req_d['name']
+    name = req_d["name"]
     details = data.get(normalize_name(name))
     if details:
         log.debug("dependency: module %s is known to pydist: %r", name, details)
         for item in details:
-            if version and version not in item.get('versions', version):
+            if version and version not in item.get("versions", version):
                 # rule doesn't match version, try next one
                 continue
-            if not item['dependency']:
+            if not item["dependency"]:
                 log.debug("dependency: requirement ignored")
-                return  # this requirement should be ignored
-            if item['dependency'].endswith(')'):
+                return None  # this requirement should be ignored
+            if item["dependency"].endswith(")"):
                 # no need to translate versions if version is hardcoded in
                 # Debian dependency
                 log.debug("dependency: requirement already has hardcoded version")
-                return item['dependency'] + env_marker_alts
-            if req_d['operator'] == '==' and req_d['version'].endswith('*'):
+                return merge_marker(item["dependency"])
+            if req_d["operator"] == "==" and req_d["version"].endswith("*"):
                 # Translate "== 1.*" to "~= 1.0"
-                req_d['operator'] = '~='
-                req_d['version'] = req_d['version'].replace('*', '0')
+                req_d["operator"] = "~="
+                req_d["version"] = req_d["version"].replace("*", "0")
                 log.debug("dependency: translated wildcard version to semver limit")
-            if req_d['version'] and (item['standard'] or item['rules']) and\
-                    req_d['operator'] not in (None, '!='):
-                o = _translate_op(req_d['operator'])
-                v = _translate(req_d['version'], item['rules'], item['standard'])
-                if req_d['operator'] == '==' and req_d['operator2'] is None:
+            if (
+                req_d["version"]
+                and (item["standard"] or item["rules"])
+                and req_d["operator"] not in (None, "!=")
+            ):
+                o = _translate_op(req_d["operator"])
+                v = _translate(req_d["version"], item["rules"], item["standard"])
+                if req_d["operator"] == "==" and req_d["operator2"] is None:
                     # Loosen for Debian revisions
                     m = re.search(r"(.*)(\d+)(\D*)$", v)
                     if m:
-                        max_v = m.group(1) + str((int(m.group(2))) + 1) + m.group(3) + "~"
+                        max_v = (
+                            m.group(1) + str((int(m.group(2))) + 1) + m.group(3) + "~"
+                        )
                     else:
                         max_v = v + ".0~"
-                    d = "%s (>= %s)%s, %s (<< %s)%s" % (
-                        item['dependency'], v, env_marker_alts,
-                        item['dependency'], max_v, env_marker_alts,
+                    d = (
+                        merge_marker(f"{item['dependency']} (>= {v})")
+                        + ", "
+                        + merge_marker(f"{item['dependency']} (<< {max_v})")
                     )
                 else:
-                    d = "%s (%s %s)%s" % (
-                        item['dependency'], o, v, env_marker_alts)
-                if req_d['version2'] and req_d['operator2'] not in (None, '!='):
-                    o2 = _translate_op(req_d['operator2'])
-                    v2 = _translate(req_d['version2'], item['rules'], item['standard'])
-                    d += ", %s (%s %s)%s" % (
-                        item['dependency'], o2, v2, env_marker_alts)
-                elif req_d['operator'] == '~=':
-                    o2 = '<<'
-                    v2 = _translate(_max_compatible(req_d['version']), item['rules'], item['standard'])
-                    d += ", %s (%s %s)%s" % (
-                        item['dependency'], o2, v2, env_marker_alts)
+                    d = merge_marker(f"{item['dependency']} ({o} {v})")
+                if req_d["version2"] and req_d["operator2"] not in (None, "!="):
+                    o2 = _translate_op(req_d["operator2"])
+                    v2 = _translate(req_d["version2"], item["rules"], item["standard"])
+                    d += ", " + merge_marker(f"{item['dependency']} ({o2} {v2})")
+                elif req_d["operator"] == "~=":
+                    o2 = "<<"
+                    v2 = _translate(
+                        _max_compatible(req_d["version"]),
+                        item["rules"],
+                        item["standard"],
+                    )
+                    d += ", " + merge_marker(f"{item['dependency']} ({o2} {v2})")
                 log.debug("dependency: constructed version")
                 return d
-            elif accept_upstream_versions and req_d['version'] and \
-                    req_d['operator'] not in (None,'!='):
-                o = _translate_op(req_d['operator'])
-                d = "%s (%s %s)%s" % (
-                    item['dependency'], o, req_d['version'], env_marker_alts)
-                if req_d['version2'] and req_d['operator2'] not in (None,'!='):
-                    o2 = _translate_op(req_d['operator2'])
-                    d += ", %s (%s %s)%s" % (
-                        item['dependency'], o2, req_d['version2'],
-                        env_marker_alts)
-                elif req_d['operator'] == '~=':
-                    o2 = '<<'
-                    d += ", %s (%s %s)%s" % (
-                        item['dependency'], o2,
-                        _max_compatible(req_d['version']), env_marker_alts)
+            elif (
+                accept_upstream_versions
+                and req_d["version"]
+                and req_d["operator"] not in (None, "!=")
+            ):
+                o = _translate_op(req_d["operator"])
+                d = merge_marker(f"{item['dependency']} ({o} {req_d['version']})")
+                if req_d["version2"] and req_d["operator2"] not in (None, "!="):
+                    o2 = _translate_op(req_d["operator2"])
+                    d += ", " + merge_marker(
+                        f"{item['dependency']} ({o2} {req_d['version2']})"
+                    )
+                elif req_d["operator"] == "~=":
+                    o2 = "<<"
+                    d += ", " + merge_marker(
+                        f"{item['dependency']} "
+                        f"({o2} {_max_compatible(req_d['version'])})"
+                    )
                 log.debug("dependency: constructed upstream version")
                 return d
             else:
-                if item['dependency'] in bdep:
-                    if None in bdep[item['dependency']] and bdep[item['dependency']][None]:
+                if item["dependency"] in bdep:
+                    if (
+                        None in bdep[item["dependency"]]
+                        and bdep[item["dependency"]][None]
+                    ):
                         log.debug("dependency: included in build-deps with limits ")
-                        return "{} ({}){}".format(
-                            item['dependency'], bdep[item['dependency']][None],
-                            env_marker_alts)
+                        return merge_marker(
+                            f"{item['dependency']} "
+                            f"({bdep[item['dependency']][None]})"
+                        )
                     # if arch in bdep[item['dependency']]:
                     # TODO: handle architecture specific dependencies from build depends
                     #       (current architecture is needed here)
                 log.debug("dependency: included in build-deps")
-                return item['dependency'] + env_marker_alts
+                return merge_marker(item["dependency"])
 
     # search for Egg metadata file or directory (using dpkg -S)
     dpkg_query_tpl, regex_filter = PYDIST_DPKG_SEARCH_TPLS[impl]
@@ -290,194 +373,256 @@ def guess_dependency(impl, req, version=
 
     log.debug("invoking dpkg -S %s", dpkg_query)
     process = subprocess.run(
-        ('/usr/bin/dpkg', '-S', dpkg_query),
-        check=False, encoding="UTF-8", capture_output=True,
+        ("/usr/bin/dpkg", "-S", dpkg_query),
+        check=False,
+        encoding="UTF-8",
+        capture_output=True,
     )
     if process.returncode == 0:
         result = set()
-        for line in process.stdout.split('\n'):
+        for line in process.stdout.split("\n"):
             if not line.strip():
                 continue
-            pkg, path = line.split(':', 1)
+            pkg, path = line.split(":", 1)
             if regex_filter and not re.search(regex_filter, path):
                 continue
             result.add(pkg)
         if len(result) > 1:
-            log.error('more than one package name found for %s dist', name)
+            log.error("more than one package name found for %s dist", name)
         elif not result:
-            log.debug('dpkg -S did not find package for %s', name)
+            log.debug("dpkg -S did not find package for %s", name)
         else:
-            log.debug('dependency: found a result with dpkg -S')
-            return result.pop() + env_marker_alts
+            log.debug("dependency: found a result with dpkg -S")
+            return merge_marker(result.pop())
     else:
-        log.debug('dpkg -S did not find package for %s: %s', name, process.stderr)
+        log.debug("dpkg -S did not find package for %s: %s", name, process.stderr)
 
     pname = sensible_pname(impl, normalize_name(name))
-    log.info('Cannot find package that provides %s. '
-             'Please add package that provides it to Build-Depends or '
-             'add "%s %s" line to %s or add proper '
-             'dependency to Depends by hand and ignore this info.',
-             name, normalize_name(name), pname, PYDIST_OVERRIDES_FNAMES[impl])
-    # return pname
+    log.info(
+        "Cannot find package that provides %s. "
+        "Please add package that provides it to Build-Depends or "
+        'add "%s %s" line to %s or add proper '
+        "dependency to Depends by hand and ignore this info.",
+        name,
+        normalize_name(name),
+        pname,
+        PYDIST_OVERRIDES_FNAMES[impl],
+    )
+    return None
 
 
-def check_environment_marker_restrictions(req, marker_str, impl):
-    """Check wither we should include or skip a dependency based on its
-    environment markers.
-
-    Returns: True  - to keep a dependency
-             False - to skip it
-             str   - to append "| foo" to generated dependencies
-    """
-    if impl != 'cpython3':
-        log.info('Ignoring environment markers for non-Python 3.x: %s', req)
-        return False
+def check_environment_marker_restrictions(
+    req: str, marker_str: str, impl: str
+) -> RequirementModification:
+    """Should we keep or skip a dependency based on its environment markers."""
+    if impl != "cpython3":
+        log.info("Ignoring environment markers for non-Python 3.x: %s", req)
+        return RequirementModification(action=ModificationAction.SKIP)
 
     try:
         marker, op, value = parse_environment_marker(marker_str)
     except ComplexEnvironmentMarker:
-        log.info('Ignoring complex environment marker: %s', req)
-        return False
+        log.info("Ignoring complex environment marker: %s", req)
+        return RequirementModification(action=ModificationAction.SKIP)
 
     # TODO: Use dynamic values when building arch-dependent
     # binaries, otherwise static values
     # TODO: Hurd values?
     supported_values = {
-        'implementation_name': ('cpython', 'pypy'),
-        'os_name': ('posix',),
-        'platform_system': ('GNU/kFreeBSD', 'Linux'),
-        'platform_machine': (platform.machine(),),
-        'platform_python_implementation': ('CPython', 'PyPy'),
-        'sys_platform': (
-            'gnukfreebsd8', 'gnukfreebsd9', 'gnukfreebsd10',
-            'gnukfreebsd11', 'gnukfreebsd12', 'gnukfreebsd13',
-            'linux'),
+        "implementation_name": ("cpython", "pypy"),
+        "os_name": ("posix",),
+        "platform_system": ("Linux",),
+        "platform_machine": (platform.machine(),),
+        "platform_python_implementation": ("CPython", "PyPy"),
+        "sys_platform": ("linux",),
     }
     if marker in supported_values:
         sv = supported_values[marker]
-        if op in ('==', '!='):
-            if ((op == '==' and value not in sv)
-                    or (op == '!=' and value in sv)):
-                log.debug('Skipping requirement (%s != %s): %s',
-                          value, sv, req)
-                return False
+        if op in ("==", "!="):
+            if op == "==" and value not in sv:
+                log.debug("Skipping requirement (%s != %s): %s", value, sv, req)
+                return RequirementModification(action=ModificationAction.SKIP)
+            if op == "!=" and len(supported_values[marker]) > 1:
+                log.debug(
+                    (
+                        "Keeping requirement as it applies to one of several "
+                        "supported environments: %s"
+                    ),
+                    req,
+                )
+                return RequirementModification(action=ModificationAction.KEEP)
         else:
             log.info(
-                'Skipping requirement with unhandled environment marker '
-                'comparison: %s', req)
-            return False
+                "Skipping requirement with unhandled environment marker "
+                "comparison: %s",
+                req,
+            )
+            return RequirementModification(action=ModificationAction.SKIP)
 
-    elif marker in ('python_version', 'python_full_version',
-                        'implementation_version'):
+    elif marker in ("python_version", "python_full_version", "implementation_version"):
         # TODO: Replace with full PEP-440 parser
         env_ver = value
-        split_ver = value.split('.')
-        if marker == 'python_version':
+        split_ver = value.split(".")
+        if marker == "python_version":
             version_parts = 2
-        elif marker == 'python_full_version':
+        elif marker == "python_full_version":
             version_parts = 3
         else:
             version_parts = len(split_ver)
 
-        if '*' in env_ver:
-            if split_ver.index('*') != len(split_ver) -1:
-                log.info('Skipping requirement with intermediate wildcard: %s',
-                         req)
-                return False
+        if "*" in env_ver:
+            if split_ver.index("*") != len(split_ver) - 1:
+                log.info("Skipping requirement with intermediate wildcard: %s", req)
+                return RequirementModification(action=ModificationAction.SKIP)
             split_ver.pop()
-            env_ver = '.'.join(split_ver)
-            if op == '==':
-                if marker == 'python_full_version':
-                    marker = 'python_version'
+            env_ver = ".".join(split_ver)
+            if op == "==":
+                if marker == "python_full_version":
+                    marker = "python_version"
                     version_parts = 2
-                else:
-                    op == '=~'
-            elif op == '!=':
-                if marker == 'python_full_version':
-                    marker = 'python_version'
+            elif op == "!=":
+                if marker == "python_full_version":
+                    marker = "python_version"
                     version_parts = 2
                 else:
-                    log.info('Ignoring wildcard != requirement, not '
-                             'representable in Debian: %s', req)
-                    return True
+                    log.info(
+                        "Ignoring wildcard != requirement, not "
+                        "representable in Debian: %s",
+                        req,
+                    )
+                    return RequirementModification(action=ModificationAction.KEEP)
             else:
-                log.info('Skipping requirement with %s on a wildcard: %s',
-                         op, req)
-                return False
+                log.info("Skipping requirement with %s on a wildcard: %s", op, req)
+                return RequirementModification(action=ModificationAction.SKIP)
 
         int_ver = []
         for ver_part in split_ver:
             if ver_part.isdigit():
                 int_ver.append(int(ver_part))
             else:
-                env_ver = '.'.join(str(x) for x in int_ver)
-                log.info('Truncating unparseable version %s to %s in %s',
-                         value, env_ver, req)
+                env_ver = ".".join(str(x) for x in int_ver)
+                log.info(
+                    "Truncating unparseable version %s to %s in %s", value, env_ver, req
+                )
                 break
 
         if len(int_ver) < version_parts:
             int_ver.append(0)
-            env_ver += '.0'
-        next_ver = int_ver.copy()
-        next_ver[version_parts - 1] += 1
-        next_ver = '.'.join(str(x) for x in next_ver)
-        prev_ver = int_ver.copy()
-        prev_ver[version_parts - 1] -= 1
-        prev_ver = '.'.join(str(x) for x in prev_ver)
+            env_ver += ".0"
+        int_next_ver = int_ver.copy()
+        int_next_ver[version_parts - 1] += 1
+        next_ver = ".".join(str(x) for x in int_next_ver)
+        # int_prev_ver = int_ver.copy()
+        # int_prev_ver[version_parts - 1] -= 1
+        # prev_ver = '.'.join(str(x) for x in int_prev_ver)
 
         # We try to do something somewhat sensible with micro versions
         # even though we don't currently emit them in python3-supported-min/max
 
-        if op == '<':
+        if op == "<":
             if int_ver <= MIN_PY_VERSION:
-                return False
-            return f'| python3-supported-min (>= {env_ver})'
-        elif op == '<=':
-            return f'| python3-supported-min (>> {env_ver})'
-        elif op == '>=':
+                return RequirementModification(action=ModificationAction.SKIP)
+            return RequirementModification(
+                action=ModificationAction.PREPEND,
+                alternative=f"python3-supported-min (>= {env_ver})",
+            )
+        elif op == "<=":
+            return RequirementModification(
+                action=ModificationAction.PREPEND,
+                alternative=f"python3-supported-min (>> {env_ver})",
+            )
+        elif op == ">=":
             if int_ver < MIN_PY_VERSION:
-                return True
-            return f'| python3-supported-max (<< {env_ver})'
-        elif op == '>':
+                return RequirementModification(action=ModificationAction.KEEP)
+            return RequirementModification(
+                action=ModificationAction.APPEND,
+                alternative=f"python3-supported-max (<< {env_ver})",
+            )
+        elif op == ">":
             if int_ver < MIN_PY_VERSION:
-                return True
-            return f'| python3-supported-max (<= {env_ver})'
-        elif op == '==':
-            if marker == 'python_version':
-                return f'| python3-supported-max (<< {env_ver}) | python3-supported-min (>= {next_ver})'
-            return f'| python3-supported-max (<< {env_ver}) | python3-supported-min (>> {env_ver})'
-        elif op == '===':
+                return RequirementModification(action=ModificationAction.KEEP)
+            return RequirementModification(
+                action=ModificationAction.APPEND,
+                alternative=f"python3-supported-max (<= {env_ver})",
+            )
+        elif op == "==":
+            if marker == "python_version":
+                return RequirementModification(
+                    action=ModificationAction.APPEND,
+                    alternative=(
+                        f"python3-supported-max (<< {env_ver}) "
+                        f"| python3-supported-min (>= {next_ver})"
+                    ),
+                )
+            return RequirementModification(
+                action=ModificationAction.APPEND,
+                alternative=(
+                    f"python3-supported-max (<< {env_ver}) "
+                    f"| python3-supported-min (>> {env_ver})"
+                ),
+            )
+        elif op == "===":
             # === is arbitrary equality (PEP 440)
-            if marker == 'python_version':
-                return f'| python3-supported-max (<< {env_ver}) | python3-supported-min (>> {env_ver})'
+            if marker == "python_version":
+                return RequirementModification(
+                    action=ModificationAction.APPEND,
+                    alternative=(
+                        f"python3-supported-max (<< {env_ver}) "
+                        f"| python3-supported-min (>> {env_ver})"
+                    ),
+                )
             else:
                 log.info(
-                    'Skipping requirement with %s environment marker, cannot '
-                    'model in Debian deps: %s', op, req)
-                return False
-        elif op == '~=':  # Compatible equality (PEP 440)
-            ceq_next_ver = int_ver[:2]
-            ceq_next_ver[1] += 1
-            ceq_next_ver = '.'.join(str(x) for x in ceq_next_ver)
-            return f'| python3-supported-max (<< {env_ver}) | python3-supported-min (>= {ceq_next_ver})'
-        elif op == '!=':
-            log.info('Ignoring != comparison in environment marker, cannot '
-                     'model in Debian deps: %s', req)
-            return True
+                    "Skipping requirement with %s environment marker, cannot "
+                    "model in Debian deps: %s",
+                    op,
+                    req,
+                )
+                return RequirementModification(action=ModificationAction.SKIP)
+        elif op == "~=":  # Compatible equality (PEP 440)
+            int_ceq_next_ver = int_ver[:2]
+            int_ceq_next_ver[1] += 1
+            ceq_next_ver = ".".join(str(x) for x in int_ceq_next_ver)
+            return RequirementModification(
+                action=ModificationAction.APPEND,
+                alternative=(
+                    f"python3-supported-max (<< {env_ver}) "
+                    f"| python3-supported-min (>= {ceq_next_ver})"
+                ),
+            )
+        elif op == "!=":
+            log.info(
+                "Ignoring != comparison in environment marker, cannot "
+                "model in Debian deps: %s",
+                req,
+            )
+            return RequirementModification(action=ModificationAction.KEEP)
 
-    elif marker == 'extra':
+    elif marker == "extra":
         # Handled in section logic of parse_requires_dist()
-        return True
+        return RequirementModification(action=ModificationAction.KEEP)
     else:
-        log.info('Skipping requirement with unknown environment marker: %s',
-                 marker)
-        return False
-    return True
-
-
-def parse_pydep(impl, fname, bdep=None,
-                *, options=None, depends_sec=None, recommends_sec=None,
-                suggests_sec=None):
+        log.info("Skipping requirement with unknown environment marker: %s", marker)
+        return RequirementModification(action=ModificationAction.SKIP)
+    return RequirementModification(action=ModificationAction.KEEP)
+
+
+class NewDependencies(TypedDict):
+    depends: list[str]
+    recommends: list[str]
+    suggests: list[str]
+
+
+def parse_pydep(
+    impl: str,
+    fname: str,
+    bdep: BD | None = None,
+    *,
+    options: Namespace | None = None,
+    depends_sec: list[str] | None = None,
+    recommends_sec: list[str] | None = None,
+    suggests_sec: list[str] | None = None,
+) -> NewDependencies:
     depends_sec = depends_sec or []
     recommends_sec = recommends_sec or []
     suggests_sec = suggests_sec or []
@@ -487,57 +632,70 @@ def parse_pydep(impl, fname, bdep=None,
     if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1:
         ver = public_dir.group(1)
 
-    guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep,
-                         accept_upstream_versions=getattr(
-                             options, 'accept_upstream_versions', False))
-
-    result = {'depends': [], 'recommends': [], 'suggests': []}
-    modified = section = False
-    env_action = True
+    guess_deps = partial(
+        guess_dependency,
+        impl=impl,
+        version=ver,
+        bdep=bdep,
+        accept_upstream_versions=getattr(options, "accept_upstream_versions", False),
+    )
+
+    result = NewDependencies(depends=[], recommends=[], suggests=[])
+    modified: bool = False
+    section: str | None = None
+    modification = RequirementModification(action=ModificationAction.KEEP)
     processed = []
-    with open(fname, 'r', encoding='utf-8') as fp:
+    with open(fname, "r", encoding="utf-8") as fp:
         for line in fp:
             line = line.strip()
-            if not line or line.startswith('#'):
+            if not line or line.startswith("#"):
                 processed.append(line)
                 continue
-            if line.startswith('['):
+            if line.startswith("["):
                 m = REQ_SECTIONS_RE.match(line)
                 if not m:
-                    log.info('Skipping section %s, unable to parse header',
-                             line)
+                    log.info("Skipping section %s, unable to parse header", line)
                     processed.append(line)
-                    section = object()
+                    # something that won't map to a result_key
+                    section = "[non-existent section]"
                     continue
-                section = m.group('section')
-                env_action = True
-                if m.group('environment_marker'):
-                    env_action = check_environment_marker_restrictions(
-                        line,
-                        m.group('environment_marker'),
-                        impl)
+                section = m.group("section")
+                modification = RequirementModification(action=ModificationAction.KEEP)
+                if m.group("environment_marker"):
+                    modification = check_environment_marker_restrictions(
+                        line, m.group("environment_marker"), impl
+                    )
                 processed.append(line)
                 continue
+            result_key: Literal["depends", "recommends", "suggests"]
             if section:
                 if section in depends_sec:
-                    result_key = 'depends'
+                    result_key = "depends"
                 elif section in recommends_sec:
-                    result_key = 'recommends'
+                    result_key = "recommends"
                 elif section in suggests_sec:
-                    result_key = 'suggests'
+                    result_key = "suggests"
                 else:
                     processed.append(line)
                     continue
             else:
-                result_key = 'depends'
+                result_key = "depends"
 
             dependency = None
-            if env_action:
+            if modification.action != ModificationAction.SKIP:
                 dependency = guess_deps(req=line)
-            if dependency and isinstance(env_action, str):
-                dependency = ', '.join(
-                    part.strip() + ' ' + env_action
-                    for part in dependency.split(','))
+            if dependency and modification.action == ModificationAction.APPEND:
+                assert modification.alternative
+                dependency = ", ".join(
+                    part.strip() + " | " + modification.alternative
+                    for part in dependency.split(",")
+                )
+            if dependency and modification.action == ModificationAction.PREPEND:
+                assert modification.alternative
+                dependency = ", ".join(
+                    modification.alternative + " | " + part.strip()
+                    for part in dependency.split(",")
+                )
 
             if dependency:
                 result[result_key].append(dependency)
@@ -545,14 +703,21 @@ def parse_pydep(impl, fname, bdep=None,
             else:
                 processed.append(line)
     if modified and public_dir:
-        with open(fname, 'w', encoding='utf-8') as fp:
-            fp.writelines(i + '\n' for i in processed)
+        with open(fname, "w", encoding="utf-8") as fp:
+            fp.writelines(i + "\n" for i in processed)
     return result
 
 
-def parse_requires_dist(impl, fname, bdep=None,
-                        *, options=None, depends_sec=None, recommends_sec=None,
-                        suggests_sec=None):
+def parse_requires_dist(
+    impl: str,
+    fname: str,
+    bdep: BD | None = None,
+    *,
+    options: Namespace | None = None,
+    depends_sec: list[str] | None = None,
+    recommends_sec: list[str] | None = None,
+    suggests_sec: list[str] | None = None,
+) -> NewDependencies:
     """Extract dependencies from a dist-info/METADATA file"""
     depends_sec = depends_sec or []
     recommends_sec = recommends_sec or []
@@ -563,26 +728,31 @@ def parse_requires_dist(impl, fname, bde
     if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1:
         ver = public_dir.group(1)
 
-    guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep,
-                         accept_upstream_versions=getattr(
-                             options, 'accept_upstream_versions', False))
-    result = {'depends': [], 'recommends': [], 'suggests': []}
+    guess_deps = partial(
+        guess_dependency,
+        impl=impl,
+        version=ver,
+        bdep=bdep,
+        accept_upstream_versions=getattr(options, "accept_upstream_versions", False),
+    )
+    result = NewDependencies(depends=[], recommends=[], suggests=[])
     section = None
-    with open(fname, 'r', encoding='utf-8') as fp:
+    with open(fname, "r", encoding="utf-8") as fp:
         metadata = email.message_from_string(fp.read())
-    requires = metadata.get_all('Requires-Dist', [])
+    requires = metadata.get_all("Requires-Dist", [])
+    result_key: Literal["depends", "recommends", "suggests"]
     for req in requires:
         m = EXTRA_RE.search(req)
-        result_key = 'depends'
+        result_key = "depends"
         if m:
-            section = m.group('section')
+            section = m.group("section")
             if section:
                 if section in depends_sec:
-                    result_key = 'depends'
+                    result_key = "depends"
                 elif section in recommends_sec:
-                    result_key = 'recommends'
+                    result_key = "recommends"
                 elif section in suggests_sec:
-                    result_key = 'suggests'
+                    result_key = "suggests"
                 else:
                     continue
         dependency = guess_deps(req=req)
@@ -592,37 +762,39 @@ def parse_requires_dist(impl, fname, bde
 
 
 # https://packaging.python.org/en/latest/specifications/simple-repository-api/#normalized-names
-def normalize_name(name):
+def normalize_name(name: str) -> str:
     """Normalize a distribution name."""
     return re.sub(r"[-_.]+", "-", name).lower()
 
 
-def sensible_pname(impl, dist_name):
+def sensible_pname(impl: str, dist_name: str) -> str:
     """Guess Debian package name from normalized distribution name."""
     dist_name = dist_name.removeprefix("python-")
     return f"{PKG_PREFIX_MAP[impl]}-{dist_name}"
 
 
-def ci_regexp(name):
+def ci_regexp(name: str) -> str:
     """Return case insensitive dpkg -S regexp."""
-    return ''.join("[%s%s]" % (i.upper(), i) if i.isalpha() else i for i in name.lower())
+    return "".join(
+        "[%s%s]" % (i.upper(), i) if i.isalpha() else i for i in name.lower()
+    )
 
 
-PEP386_PRE_VER_RE = re.compile(r'[-.]?(alpha|beta|rc|dev|a|b|c)')
-PEP440_PRE_VER_RE = re.compile(r'[-.]?(a|b|rc)')
-GROUP_RE = re.compile(r'\$(\d+)')
+PEP386_PRE_VER_RE = re.compile(r"[-.]?(alpha|beta|rc|dev|a|b|c)")
+PEP440_PRE_VER_RE = re.compile(r"[-.]?(a|b|rc)")
+GROUP_RE = re.compile(r"\$(\d+)")
 
 
-def _pl2py(pattern):
+def _pl2py(pattern: str) -> str:
     r"""Convert Perl RE patterns used in uscan to Python's
 
     >>> print(_pl2py('foo$3'))
     foo\g<3>
     """
-    return GROUP_RE.sub(r'\\g<\1>', pattern)
+    return GROUP_RE.sub(r"\\g<\1>", pattern)
 
 
-def _max_compatible(version):
+def _max_compatible(version: str) -> str:
     """Return the maximum version compatible with `version` in PEP440 terms,
     used by ~= requires version specifiers.
 
@@ -648,7 +820,7 @@ def _max_compatible(version):
     return str(v + 1)
 
 
-def _translate(version, rules, standard):
+def _translate(version: str, rules: list[str], standard: Standard | None) -> str:
     """Translate Python version into Debian one.
 
     >>> _translate('1.C2betac', ['s/c//gi'], None)
@@ -661,33 +833,33 @@ def _translate(version, rules, standard)
     """
     for rule in rules:
         # uscan supports s, tr and y operations
-        if rule.startswith(('tr', 'y')):
+        if rule.startswith(("tr", "y")):
             # Note: no support for escaped separator in the pattern
-            pos = 1 if rule.startswith('y') else 2
-            tmp = rule[pos + 1:].split(rule[pos])
+            pos = 1 if rule.startswith("y") else 2
+            tmp = rule[pos + 1 :].split(rule[pos])
             version = version.translate(str.maketrans(tmp[0], tmp[1]))
-        elif rule.startswith('s'):
+        elif rule.startswith("s"):
             # uscan supports: g, u and x flags
             tmp = rule[2:].split(rule[1])
             pattern = re.compile(tmp[0])
             count = 1
             if tmp[2:]:
                 flags = tmp[2]
-                if 'g' in flags:
+                if "g" in flags:
                     count = 0
-                if 'i' in flags:
+                if "i" in flags:
                     pattern = re.compile(tmp[0], re.I)
             version = pattern.sub(_pl2py(tmp[1]), version, count)
         else:
-            log.warning('unknown rule ignored: %s', rule)
-    if standard == 'PEP386':
-        version = PEP386_PRE_VER_RE.sub(r'~\g<1>', version)
-    elif standard == 'PEP440':
-        version = PEP440_PRE_VER_RE.sub(r'~\g<1>', version)
+            log.warning("unknown rule ignored: %s", rule)
+    if standard == Standard.PEP386:
+        version = PEP386_PRE_VER_RE.sub(r"~\g<1>", version)
+    elif standard == Standard.PEP440:
+        version = PEP440_PRE_VER_RE.sub(r"~\g<1>", version)
     return version
 
 
-def _translate_op(operator):
+def _translate_op(operator: str) -> str:
     """Translate Python version operator into Debian one.
 
     >>> _translate_op('==')
@@ -700,16 +872,16 @@ def _translate_op(operator):
     return DEB_VERS_OPS.get(operator, operator)
 
 
-if __name__ == '__main__':
-    impl = os.environ.get('IMPL', 'cpython3')
+if __name__ == "__main__":
+    impl = os.environ.get("IMPL", "cpython3")
     for i in sys.argv[1:]:
         if os.path.isfile(i):
             try:
-                print(', '.join(parse_pydep(impl, i)['depends']))
+                print(", ".join(parse_pydep(impl, i)["depends"]))
             except Exception as err:
-                log.error('%s: cannot guess (%s)', i, err)
+                log.error("%s: cannot guess (%s)", i, err)
         else:
             try:
-                print(guess_dependency(impl, i) or '')
+                print(guess_dependency(impl, i) or "")
             except Exception as err:
-                log.error('%s: cannot guess (%s)', i, err)
+                log.error("%s: cannot guess (%s)", i, err)
diff -pruN 6.20251204.1/dhpython/tools.py 6.20251221/dhpython/tools.py
--- 6.20251204.1/dhpython/tools.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/tools.py	2025-12-21 21:07:09.000000000 +0000
@@ -22,37 +22,41 @@
 import logging
 import os
 import re
+from contextlib import ExitStack
 from datetime import datetime
+from functools import cache
 from glob import glob
-from pickle import dumps
 from shutil import rmtree
 from os.path import exists, getsize, isdir, islink, join, split
+from pathlib import Path
 from subprocess import Popen, PIPE
+from typing import Callable, Iterable, Literal, TextIO, NamedTuple, overload
 
-log = logging.getLogger('dhpython')
-EGGnPTH_RE = re.compile(r'(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$')
-SHAREDLIB_RE = re.compile(r'NEEDED.*libpython(\d\.\d)')
 
+log = logging.getLogger("dhpython")
+EGGnPTH_RE = re.compile(r"(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$")
+SHAREDLIB_RE = re.compile(r"NEEDED.*libpython(\d\.\d)")
 
-def relpath(target, link):
+
+def relpath(target: str, link: str) -> str:
     """Return relative path.
 
     >>> relpath('/usr/share/python-foo/foo.py', '/usr/bin/foo', )
     '../share/python-foo/foo.py'
     """
-    t = target.split('/')
-    l = link.split('/')
+    t = target.split("/")
+    l = link.split("/")
     while l and l[0] == t[0]:
         del l[0], t[0]
-    return '/'.join(['..'] * (len(l) - 1) + t)
+    return "/".join([".."] * (len(l) - 1) + t)
 
 
-def relative_symlink(target, link):
+def relative_symlink(target: str, link: str) -> None:
     """Create relative symlink."""
     return os.symlink(relpath(target, link), link)
 
 
-def move_file(fpath, dstdir):
+def move_file(fpath: str, dstdir: str) -> None:
     """Move file to dstdir. Works with symlinks (including relative ones)."""
     if isdir(fpath):
         dname = split(fpath)[-1]
@@ -67,7 +71,13 @@ def move_file(fpath, dstdir):
         os.rename(fpath, dstdir)
 
 
-def move_matching_files(src, dst, pattern, sub=None, repl=''):
+def move_matching_files(
+    src: str,
+    dst: str,
+    pattern: str,
+    sub: str | None = None,
+    repl: str | None = None,
+) -> None:
     r"""Move files (preserving path) that match given pattern.
 
     move_matching_files('foo/bar/', 'foo/baz/', r'spam/.*\.so$')
@@ -77,20 +87,22 @@ def move_matching_files(src, dst, patter
     :param repl: replacement for `sub`
     """
     match = re.compile(pattern).search
+    sub_f: Callable[[str, str], str] | None = None
     if sub:
-        sub = re.compile(sub).sub
-        repl = repl or ''
+        sub_f = re.compile(sub).sub
+        repl = repl or ""
     for root, _, filenames in os.walk(src):
         for fn in filenames:
             spath = join(root, fn)
             if match(spath):
-                if sub is not None:
-                    spath = sub(repl, spath)
+                if sub_f is not None:
+                    assert repl
+                    spath = sub_f(repl, spath)
                 dpath = join(dst, relpath(spath, src))
                 os.renames(spath, dpath)
 
 
-def fix_shebang(fpath, replacement=None):
+def fix_shebang(fpath: str | Path, replacement: str | None = None) -> bool | None:
     """Normalize file's shebang.
 
     :param replacement: new shebang command (path to interpreter and options)
@@ -98,35 +110,35 @@ def fix_shebang(fpath, replacement=None)
     try:
         interpreter = Interpreter.from_file(fpath)
     except Exception as err:
-        log.debug('fix_shebang (%s): %s', fpath, err)
+        log.debug("fix_shebang (%s): %s", fpath, err)
         return None
 
     if not replacement:
-        if interpreter.version == '3':
+        if interpreter.version == "3":
             # Potentially an unversioned shebang (e.g. produced by setuptools >= 76)
-            replacement = '/usr/bin/python3'
+            replacement = "/usr/bin/python3"
             if interpreter.debug:
-                replacement += '-dbg'
-        elif interpreter.path != '/usr/bin/':  # f.e. /usr/local/* or */bin/env
-            interpreter.path = '/usr/bin'
+                replacement += "-dbg"
+        elif interpreter.path != "/usr/bin/":  # f.e. /usr/local/* or */bin/env
+            interpreter.path = "/usr/bin"
             replacement = repr(interpreter)
     if replacement:
-        log.info('replacing shebang in %s', fpath)
+        log.info("replacing shebang in %s", fpath)
         try:
-            with open(fpath, 'rb') as fp:
+            with open(fpath, "rb") as fp:
                 fcontent = fp.readlines()
         except IOError:
-            log.error('cannot open %s', fpath)
+            log.error("cannot open %s", fpath)
             return False
         # do not catch IOError here, the file is zeroed at this stage so it's
         # better to fail
-        with open(fpath, 'wb') as fp:
-            fp.write(("#! %s\n" % replacement).encode('utf-8'))
+        with open(fpath, "wb") as fp:
+            fp.write(("#! %s\n" % replacement).encode("utf-8"))
             fp.writelines(fcontent[1:])
     return True
 
 
-def so2pyver(fpath):
+def so2pyver(fpath: str) -> "Version | None":
     """Return libpython version file is linked to or None.
 
     :rtype: tuple
@@ -135,12 +147,14 @@ def so2pyver(fpath):
 
     cmd = "readelf -Wd '%s'" % fpath
     with Popen(cmd, stdout=PIPE, shell=True) as process:
+        assert process.stdout
         match = SHAREDLIB_RE.search(str(process.stdout.read(), encoding="UTF-8"))
         if match:
             return Version(match.groups()[0])
+    return None
 
 
-def clean_egg_name(name):
+def clean_egg_name(name: str) -> str:
     """Remove Python version and platform name from Egg files/dirs.
 
     >>> clean_egg_name('python_pipeline-0.1.3_py3k-py3.1.egg-info')
@@ -150,34 +164,42 @@ def clean_egg_name(name):
     """
     match = EGGnPTH_RE.match(name)
     if match and match.group(2) is not None:
-        return ''.join(match.group(1, 3))
+        return "".join(match.group(1, 3))
     return name
 
 
-def parse_ns(fpaths, other=None):
+def parse_ns(
+    fpaths: Iterable[str],
+    other: Iterable[str] | None = None,
+) -> set[str]:
     """Parse namespace_packages.txt files."""
     result = set(other or [])
     for fpath in fpaths:
-        with open(fpath, 'r', encoding='utf-8') as fp:
+        with open(fpath, "r", encoding="utf-8") as fp:
             for line in fp:
                 if line:
                     result.add(line.strip())
     return result
 
 
-def remove_ns(interpreter, package, namespaces, versions):
+def remove_ns(
+    interpreter: "Interpreter",
+    package: str,
+    namespaces: Iterable[str],
+    versions: "Iterable[Version]",
+) -> set[str]:
     """Remove empty __init__.py files for requested namespaces."""
     if not isinstance(namespaces, set):
         namespaces = set(namespaces)
     keep = set()
     for ns in namespaces:
         for version in versions:
-            fpath = join(interpreter.sitedir(package, version), *ns.split('.'))
-            fpath = join(fpath, '__init__.py')
+            fpath = join(interpreter.sitedir(package, version), *ns.split("."))
+            fpath = join(fpath, "__init__.py")
             if not exists(fpath):
                 continue
             if getsize(fpath) != 0:
-                log.warning('file not empty, cannot share %s namespace', ns)
+                log.warning("file not empty, cannot share %s namespace", ns)
                 keep.add(ns)
                 break
 
@@ -187,15 +209,15 @@ def remove_ns(interpreter, package, name
     # remove empty __init__.py files, if available
     for ns in result:
         for version in versions:
-            dpath = join(interpreter.sitedir(package, version), *ns.split('.'))
-            fpath = join(dpath, '__init__.py')
+            dpath = join(interpreter.sitedir(package, version), *ns.split("."))
+            fpath = join(dpath, "__init__.py")
             if exists(fpath):
                 os.remove(fpath)
                 if not os.listdir(dpath):
                     os.rmdir(dpath)
         # clean pyshared dir as well
-        dpath = join('debian', package, 'usr/share/pyshared', *ns.split('.'))
-        fpath = join(dpath, '__init__.py')
+        dpath = join("debian", package, "usr/share/pyshared", *ns.split("."))
+        fpath = join(dpath, "__init__.py")
         if exists(fpath):
             os.remove(fpath)
             if not os.listdir(dpath):
@@ -203,7 +225,46 @@ def remove_ns(interpreter, package, name
     return result
 
 
-def execute(command, cwd=None, env=None, log_output=None, shell=True):
+class ExecutionResult(NamedTuple):
+    returncode: int
+
+
+class ExecutionResultWithOutput(NamedTuple):
+    returncode: int
+    stdout: str
+    stderr: str
+
+
+@overload
+def execute(
+    command: str | list[str],
+    *,
+    cwd: str | None = None,
+    env: dict[str, str] | None = None,
+    log_output: None = None,
+    shell: bool = True,
+) -> ExecutionResultWithOutput: ...
+
+
+@overload
+def execute(
+    command: str | list[str],
+    *,
+    cwd: str | None = None,
+    env: dict[str, str] | None = None,
+    log_output: TextIO | str | Literal[False],
+    shell: bool = True,
+) -> ExecutionResult: ...
+
+
+def execute(
+    command: str | list[str],
+    *,
+    cwd: str | None = None,
+    env: dict[str, str] | None = None,
+    log_output: TextIO | str | Literal[False] | None = None,
+    shell: bool = True,
+) -> ExecutionResult | ExecutionResultWithOutput:
     """Execute external shell command.
 
     :param cdw: current working directory
@@ -213,59 +274,58 @@ def execute(command, cwd=None, env=None,
         * None if output should be included in the returned dict, or
         * False if output should be redirected to stdout/stderr
     """
-    args = {'shell': shell, 'cwd': cwd, 'env': env}
-    close = False
-    if log_output is False:
-        pass
-    elif log_output is None:
-        args.update(stdout=PIPE, stderr=PIPE)
-    elif log_output:
-        if isinstance(log_output, str):
-            close = True
-            # pylint: disable=consider-using-with
-            log_output = open(log_output, 'a', encoding='utf-8')
-        log_output.write('\n# command executed on {}'.format(datetime.now().isoformat()))
-        log_output.write('\n$ {}\n'.format(command))
-        log_output.flush()
-        args.update(stdout=log_output, stderr=log_output)
-
-    log.debug('invoking: %s', command)
-    with Popen(command, **args) as process:
-        stdout, stderr = process.communicate()
-        if close:
-            log_output.close()
-        return {
-            "returncode": process.returncode,
-            "stdout": stdout is not None and str(stdout, 'utf-8'),
-            "stderr": stderr is not None and str(stderr, 'utf-8'),
-        }
-
-
-class memoize:
-    def __init__(self, func):
-        self.func = func
-        self.cache = {}
-
-    def __call__(self, *args, **kwargs):
-        key = dumps((args, kwargs))
-        if key not in self.cache:
-            self.cache[key] = self.func(*args, **kwargs)
-        return self.cache[key]
+    with ExitStack() as stack:
+        output: TextIO | int | None
+        if log_output is False:
+            output = None
+        elif log_output is None:
+            output = PIPE
+        elif log_output:
+            if isinstance(log_output, str):
+                log_output = stack.enter_context(
+                    open(log_output, "a", encoding="utf-8")
+                )
+            assert isinstance(log_output, TextIO)
+            log_output.write(
+                "\n# command executed on {}".format(datetime.now().isoformat())
+            )
+            log_output.write("\n$ {}\n".format(command))
+            log_output.flush()
+            output = log_output
+
+        log.debug("invoking: %s", command)
+        with Popen(
+            command,
+            shell=shell,
+            cwd=cwd,
+            env=env,
+            encoding="utf-8",
+            stdout=output,
+            stderr=output,
+        ) as process:
+            stdout_text, stderr_text = process.communicate()
+            if output == PIPE:
+                return ExecutionResultWithOutput(
+                    returncode=process.returncode,
+                    stdout=stdout_text,
+                    stderr=stderr_text,
+                )
+            return ExecutionResult(returncode=process.returncode)
 
 
-@memoize
-def dpkg_architecture():
+@cache
+def dpkg_architecture() -> dict[str, str]:
     """Parse dpkg-architecture output"""
-    arch_data = {}
-    if exists('/usr/bin/dpkg-architecture'):
-        res = execute('/usr/bin/dpkg-architecture')
-        for line in res['stdout'].splitlines():
-            key, value = line.strip().split('=', 1)
+    arch_data: dict[str, str] = {}
+    if exists("/usr/bin/dpkg-architecture"):
+        res = execute("/usr/bin/dpkg-architecture")
+        for line in res.stdout.splitlines():
+            key, value = line.strip().split("=", 1)
             arch_data[key] = value
     return arch_data
 
 
-def pyinstall(interpreter, package, vrange):
+def pyinstall(interpreter: "Interpreter", package: str, vrange: str) -> None:
     """Install local files listed in pkg.pyinstall files as public modules."""
     srcfpath = "./debian/%s.pyinstall" % package
     if not exists(srcfpath):
@@ -273,29 +333,29 @@ def pyinstall(interpreter, package, vran
     impl = interpreter.impl
     versions = get_requested_versions(impl, vrange)
 
-    with open(srcfpath, encoding='utf-8') as fp:
+    with open(srcfpath, encoding="utf-8") as fp:
         for line in fp:
-            if not line.strip() or line.startswith('#'):
+            if not line.strip() or line.startswith("#"):
                 continue
-            details = INSTALL_RE.match(line)
-            if not details:
+            if not (m := INSTALL_RE.match(line)):
                 raise ValueError("unrecognized line: %s" % line)
-            details = details.groupdict()
-            if details['module']:
-                details['module'] = details['module'].replace('.', '/')
-            myvers = versions & get_requested_versions(impl, details['vrange'])
+            details = m.groupdict()
+            if details["module"]:
+                details["module"] = details["module"].replace(".", "/")
+            myvers = versions & get_requested_versions(impl, details["vrange"])
             if not myvers:
-                log.debug('%s.pyinstall: no matching versions for line %s',
-                        package, line)
+                log.debug(
+                    "%s.pyinstall: no matching versions for line %s", package, line
+                )
                 continue
-            files = glob(details['pattern'])
+            files = glob(details["pattern"])
             if not files:
-                raise ValueError("missing file(s): %s" % details['pattern'])
+                raise ValueError("missing file(s): %s" % details["pattern"])
             for fpath in files:
-                fpath = fpath.lstrip('/.')
-                if details['module']:
-                    dstname = join(details['module'], split(fpath)[1])
-                elif fpath.startswith('debian/'):
+                fpath = fpath.lstrip("/.")
+                if details["module"]:
+                    dstname = join(details["module"], split(fpath)[1])
+                elif fpath.startswith("debian/"):
                     dstname = fpath[7:]
                 else:
                     dstname = fpath
@@ -309,7 +369,7 @@ def pyinstall(interpreter, package, vran
                     os.link(fpath, dstfpath)
 
 
-def pyremove(interpreter, package, vrange):
+def pyremove(interpreter: "Interpreter", package: str, vrange: str) -> None:
     """Remove public modules listed in pkg.pyremove file."""
     srcfpath = "./debian/%s.pyremove" % package
     if not exists(srcfpath):
@@ -317,41 +377,51 @@ def pyremove(interpreter, package, vrang
     impl = interpreter.impl
     versions = get_requested_versions(impl, vrange)
 
-    with open(srcfpath, encoding='utf-8') as fp:
+    with open(srcfpath, encoding="utf-8") as fp:
         for line in fp:
-            if not line.strip() or line.startswith('#'):
+            if not line.strip() or line.startswith("#"):
                 continue
-            details = REMOVE_RE.match(line)
-            if not details:
+            if not (m := REMOVE_RE.match(line)):
                 raise ValueError("unrecognized line: %s: %s" % (package, line))
-            details = details.groupdict()
-            myvers = versions & get_requested_versions(impl, details['vrange'])
+            details = m.groupdict()
+            myvers = versions & get_requested_versions(impl, details["vrange"])
             if not myvers:
-                log.debug('%s.pyremove: no matching versions for line %s',
-                        package, line)
+                log.debug(
+                    "%s.pyremove: no matching versions for line %s", package, line
+                )
             for version in myvers:
                 site_dirs = interpreter.old_sitedirs(package, version)
                 site_dirs.append(interpreter.sitedir(package, version))
                 for sdir in site_dirs:
-                    files = glob(sdir + '/' + details['pattern'])
+                    files = glob(sdir + "/" + details["pattern"])
                     for fpath in files:
                         if isdir(fpath):
                             rmtree(fpath)
                         else:
                             os.remove(fpath)
 
+
 from dhpython.interpreter import Interpreter
 from dhpython.version import Version, get_requested_versions, RANGE_PATTERN
-INSTALL_RE = re.compile(r"""
+
+INSTALL_RE = re.compile(
+    r"""
     (?P<pattern>.+?)  # file pattern
     (?:\s+  # optional Python module name:
     (?P<module>[A-Za-z][A-Za-z0-9_.]*)?
     )?
     \s*  # optional version range:
     (?P<vrange>%s)?$
-""" % RANGE_PATTERN, re.VERBOSE)
-REMOVE_RE = re.compile(r"""
+"""
+    % RANGE_PATTERN,
+    re.VERBOSE,
+)
+REMOVE_RE = re.compile(
+    r"""
     (?P<pattern>.+?)  # file pattern
     \s*  # optional version range:
     (?P<vrange>%s)?$
-""" % RANGE_PATTERN, re.VERBOSE)
+"""
+    % RANGE_PATTERN,
+    re.VERBOSE,
+)
diff -pruN 6.20251204.1/dhpython/version.py 6.20251221/dhpython/version.py
--- 6.20251204.1/dhpython/version.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/dhpython/version.py	2025-12-21 21:07:09.000000000 +0000
@@ -20,27 +20,47 @@
 
 import logging
 import re
+from typing import Iterable
 from os.path import exists
 
 from dhpython import _defaults
 
-RANGE_PATTERN = r'(-)?(\d\.\d+)(?:(-)(\d\.\d+)?)?'
+RANGE_PATTERN = r"(-)?(\d\.\d+)(?:(-)(\d\.\d+)?)?"
 RANGE_RE = re.compile(RANGE_PATTERN)
-VERSION_RE = re.compile(r'''
+VERSION_RE = re.compile(
+    r"""
     (?P<major>\d+)\.?
     (?P<minor>\d+)?\.?
     (?P<micro>\d+)?[.\s]?
     (?P<releaselevel>alpha|beta|candidate|final)?[.\s]?
-    (?P<serial>\d+)?''', re.VERBOSE)
+    (?P<serial>\d+)?""",
+    re.VERBOSE,
+)
 
-log = logging.getLogger('dhpython')
+log = logging.getLogger("dhpython")
 Interpreter = None
 
+type V = "str | Version | Iterable[int]"
+
 
 class Version:
+    major: int
+    minor: int | None
+    micro: int | None
+    releaselevel: str | None
+    serial: int | None
+
     # TODO: Upgrade to PEP-440
-    def __init__(self, value=None, major=None, minor=None, micro=None,
-                 releaselevel=None, serial=None):
+    def __init__(
+        self,
+        value: V | None = None,
+        *,
+        major: int | None = None,
+        minor: int | None = None,
+        micro: int | None = None,
+        releaselevel: str | None = None,
+        serial: int | None = None,
+    ) -> None:
         # pylint: disable=too-many-positional-arguments
         """Construct a new instance.
 
@@ -51,27 +71,29 @@ class Version:
         """
         # pylint: disable=unused-argument
         if isinstance(value, (tuple, list)):
-            value = '.'.join(str(i) for i in value)
+            value = ".".join(str(i) for i in value)
         if isinstance(value, Version):
-            for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
+            for name in ("major", "minor", "micro", "releaselevel", "serial"):
                 setattr(self, name, getattr(value, name))
             return
         comp = locals()
-        del comp['self']
-        del comp['value']
+        del comp["self"]
+        del comp["value"]
         if value:
+            assert isinstance(value, str)
             match = VERSION_RE.match(value)
             for name, value in match.groupdict().items() if match else []:
                 if value is not None and comp[name] is None:
-                    comp[name] = value
+                    if name == "releaselevel":
+                        comp[name] = value
+                    else:
+                        comp[name] = int(value)
         for name, value in comp.items():
-            if name != 'releaselevel' and value is not None:
-                value = int(value)
             setattr(self, name, value)
         if self.major is None:
-            raise ValueError('major component is required')
+            raise ValueError("major component is required")
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return major.minor or major string.
 
         >>> str(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
@@ -81,13 +103,13 @@ class Version:
         """
         result = str(self.major)
         if self.minor is not None:
-            result += '.{}'.format(self.minor)
+            result += ".{}".format(self.minor)
         return result
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(repr(self))
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         """Return full version string.
 
         >>> repr(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
@@ -96,14 +118,14 @@ class Version:
         "Version('2')"
         """
         result = "Version('{}".format(self)
-        for name in ('micro', 'releaselevel', 'serial'):
+        for name in ("micro", "releaselevel", "serial"):
             value = getattr(self, name)
             if not value:
                 break
-            result += '.{}'.format(value)
+            result += ".{}".format(value)
         return result + "')"
 
-    def __add__(self, other):
+    def __add__(self, other: int | str) -> "Version":
         """Return next version.
 
         >>> Version('3.1') + 1
@@ -112,13 +134,13 @@ class Version:
         Version('3')
         """
         result = Version(self)
-        if self.minor is None:
+        if result.minor is None:
             result.major += int(other)
         else:
             result.minor += int(other)
         return result
 
-    def __sub__(self, other):
+    def __sub__(self, other: int | str) -> "Version":
         """Return previous version.
 
         >>> Version('3.1') - 1
@@ -127,36 +149,36 @@ class Version:
         Version('2')
         """
         result = Version(self)
-        if self.minor is None:
+        if result.minor is None:
             result.major -= int(other)
             new = result.major
         else:
             result.minor -= int(other)
             new = result.minor
         if new < 0:
-            raise ValueError('cannot decrease version further')
+            raise ValueError("cannot decrease version further")
         return result
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         try:
-            other = Version(other)
+            other = Version(other)  # type: ignore
         except Exception:
             return False
         return self.__cmp(other) == 0
 
-    def __lt__(self, other):
+    def __lt__(self, other: object) -> bool:
         return self.__cmp(other) < 0
 
-    def __le__(self, other):
+    def __le__(self, other: object) -> bool:
         return self.__cmp(other) <= 0
 
-    def __gt__(self, other):
+    def __gt__(self, other: object) -> bool:
         return self.__cmp(other) > 0
 
-    def __ge__(self, other):
+    def __ge__(self, other: object) -> bool:
         return self.__cmp(other) >= 0
 
-    def __lshift__(self, other):
+    def __lshift__(self, other: V) -> bool:
         """Compare major.minor or major only (if minor is not set).
 
         >>> Version('2.6') << Version('2.7')
@@ -173,11 +195,11 @@ class Version:
         if not isinstance(other, Version):
             other = Version(other)
         if self.minor is None or other.minor is None:
-            return self.__cmp(other, ignore='minor') < 0
+            return self.__cmp(other, ignore="minor") < 0
         else:
-            return self.__cmp(other, ignore='micro') < 0
+            return self.__cmp(other, ignore="micro") < 0
 
-    def __rshift__(self, other):
+    def __rshift__(self, other: V) -> bool:
         """Compare major.minor or major only (if minor is not set).
 
         >>> Version('2.6') >> Version('2.7')
@@ -194,22 +216,31 @@ class Version:
         if not isinstance(other, Version):
             other = Version(other)
         if self.minor is None or other.minor is None:
-            return self.__cmp(other, ignore='minor') > 0
+            return self.__cmp(other, ignore="minor") > 0
         else:
-            return self.__cmp(other, ignore='micro') > 0
+            return self.__cmp(other, ignore="micro") > 0
 
-    def __cmp(self, other, ignore=None):
+    def __cmp(self, other: object, ignore: str | None = None) -> int:
         if not isinstance(other, Version):
-            other = Version(other)
-        for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
+            try:
+                other = Version(other)  # type: ignore
+            except Exception:
+                raise ValueError(f"Cannot compare Version with {other!r}")
+        for name in ("major", "minor", "micro", "releaselevel", "serial"):
             if name == ignore:
                 break
-            value1 = getattr(self, name) or 0
-            value2 = getattr(other, name) or 0
-            if name == 'releaselevel':
-                rmap = {'alpha': -3, 'beta': -2, 'candidate': -1, 'final': 0}
-                value1 = rmap.get(value1, 0)
-                value2 = rmap.get(value2, 0)
+            if name == "releaselevel":
+                rmap: dict[str | None, int] = {
+                    "alpha": -3,
+                    "beta": -2,
+                    "candidate": -1,
+                    "final": 0,
+                }
+                value1 = rmap.get(self.releaselevel, 0)
+                value2 = rmap.get(other.releaselevel, 0)
+            else:
+                value1 = getattr(self, name) or 0
+                value2 = getattr(other, name) or 0
             if value1 == value2:
                 continue
             return (value1 > value2) - (value1 < value2)
@@ -217,7 +248,15 @@ class Version:
 
 
 class VersionRange:
-    def __init__(self, value=None, minver=None, maxver=None):
+    minver: Version | None
+    maxver: Version | None
+
+    def __init__(
+        self,
+        value: str | None = None,
+        minver: V | None = None,
+        maxver: V | None = None,
+    ) -> None:
         if minver:
             self.minver = Version(minver)
         else:
@@ -234,12 +273,12 @@ class VersionRange:
             if maxver and self.maxver is None:
                 self.maxver = maxver
 
-    def __bool__(self):
+    def __bool__(self) -> bool:
         if self.minver is not None or self.maxver is not None:
             return True
         return False
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return version range string from given range.
 
         >>> str(VersionRange(minver='3.4'))
@@ -256,17 +295,17 @@ class VersionRange:
         '-'
         """
         if self.minver is None is self.maxver:
-            return '-'
+            return "-"
         if self.minver == self.maxver:
             return str(self.minver)
         elif self.minver is None:
-            return '-{}'.format(self.maxver)
+            return "-{}".format(self.maxver)
         elif self.maxver is None:
-            return '{}-'.format(self.minver)
+            return "{}-".format(self.minver)
         else:
-            return '{}-{}'.format(self.minver, self.maxver)
+            return "{}-{}".format(self.minver, self.maxver)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         """Return version range string.
 
         >>> repr(VersionRange('5.0-'))
@@ -274,16 +313,17 @@ class VersionRange:
         >>> repr(VersionRange('3.0-3.5'))
         "VersionRange(minver='3.0', maxver='3.5')"
         """
-        result = 'VersionRange('
+        result = "VersionRange("
         if self.minver is not None:
-            result += "minver='{}'".format(self.minver)
+            result += f"minver='{self.minver}'"
+            if self.maxver is not None:
+                result += ", "
         if self.maxver is not None:
-            result += ", maxver='{}'".format(self.maxver)
-        result = result.replace('(, ', '(')
+            result += f"maxver='{self.maxver}'"
         return result + ")"
 
     @staticmethod
-    def parse(value):
+    def parse(value: str) -> tuple[Version | None, Version | None]:
         """Return minimum and maximum Python version from given range.
 
         >>> VersionRange.parse('3.0-')
@@ -301,7 +341,7 @@ class VersionRange:
         >>> VersionRange.parse('>= 4.0')
         (Version('4.0'), None)
         """
-        if value in ('', '-'):
+        if value in ("", "-"):
             return None, None
 
         match = RANGE_RE.match(value)
@@ -317,15 +357,16 @@ class VersionRange:
                 minv = Version(groups[1])
                 return minv, minv
 
-            minv = maxv = None
+            minv_s: str | None = None
+            maxv_s: str | None = None
             if groups[0]:  # maximum version only
-                maxv = groups[1]
+                maxv_s = groups[1]
             else:
-                minv = groups[1]
-                maxv = groups[3]
+                minv_s = groups[1]
+                maxv_s = groups[3]
 
-            minv = Version(minv) if minv else None
-            maxv = Version(maxv) if maxv else None
+            minv = Version(minv_s) if minv_s else None
+            maxv = Version(maxv_s) if maxv_s else None
 
         if maxv and minv and minv > maxv:
             raise ValueError("version range is invalid: %s" % value)
@@ -333,7 +374,7 @@ class VersionRange:
         return minv, maxv
 
     @staticmethod
-    def _parse_pycentral(value):
+    def _parse_pycentral(value: str) -> tuple[Version | None, Version | None]:
         """Parse X-Python3-Version.
 
         >>> VersionRange._parse_pycentral('>= 3.10')
@@ -349,18 +390,18 @@ class VersionRange:
         minv = maxv = None
         hardcoded = set()
 
-        for item in value.split(','):
+        for item in value.split(","):
             item = item.strip()
 
-            match = re.match(r'>=\s*([\d\.]+)', item)
+            match = re.match(r">=\s*([\d\.]+)", item)
             if match:
                 minv = match.group(1)
                 continue
-            match = re.match(r'<<\s*([\d\.]+)', item)
+            match = re.match(r"<<\s*([\d\.]+)", item)
             if match:
                 maxv = match.group(1)
                 continue
-            match = re.match(r'^[\d\.]+$', item)
+            match = re.match(r"^[\d\.]+$", item)
             if match:
                 hardcoded.add(match.group(0))
 
@@ -375,7 +416,7 @@ class VersionRange:
         return Version(minv) if minv else None, Version(maxv) if maxv else None
 
 
-def default(impl):
+def default(impl: str) -> Version:
     """Return default interpreter version for given implementation."""
     if impl not in _defaults.DEFAULT:
         raise ValueError("interpreter implementation not supported: %r" % impl)
@@ -383,7 +424,7 @@ def default(impl):
     return Version(major=ver[0], minor=ver[1])
 
 
-def supported(impl):
+def supported(impl: str) -> list[Version]:
     """Return list of supported interpreter versions for given implementation."""
     if impl not in _defaults.SUPPORTED:
         raise ValueError("interpreter implementation not supported: %r" % impl)
@@ -391,7 +432,11 @@ def supported(impl):
     return [Version(major=v[0], minor=v[1]) for v in versions]
 
 
-def get_requested_versions(impl, vrange=None, available=None):
+def get_requested_versions(
+    impl: str,
+    vrange: str | None = None,
+    available: bool | None = None,
+) -> set[Version]:
     """Return a set of requested and supported Python versions.
 
     :param impl: interpreter implementation
@@ -407,37 +452,44 @@ def get_requested_versions(impl, vrange=
     >>> get_requested_versions('cpython3', '>= 5.0')
     set()
     """
+    from dhpython.interpreter import Interpreter
+
+    parsed_vrange: VersionRange | None = None
     if isinstance(vrange, str):
-        vrange = VersionRange(vrange)
+        parsed_vrange = VersionRange(vrange)
 
-    if not vrange:
+    if not parsed_vrange:
         versions = set(supported(impl))
     else:
-        minv = Version(major=0, minor=0) if vrange.minver is None else vrange.minver
-        maxv = Version(major=99, minor=99) if vrange.maxver is None else vrange.maxver
+        minv = (
+            Version(major=0, minor=0)
+            if parsed_vrange.minver is None
+            else parsed_vrange.minver
+        )
+        maxv = (
+            Version(major=99, minor=99)
+            if parsed_vrange.maxver is None
+            else parsed_vrange.maxver
+        )
         if minv == maxv:
             versions = set([minv] if minv in supported(impl) else tuple())
         else:
             versions = set(v for v in supported(impl) if minv <= v < maxv)
 
-    if available is not None:
-        # to avoid circular imports
-        global Interpreter
-        if Interpreter is None:
-            from dhpython.interpreter import Interpreter
     if available:
         interpreter = Interpreter(impl=impl)
-        versions = set(v for v in versions
-                       if exists(interpreter.binary(v)))
+        versions = set(v for v in versions if exists(interpreter.binary(v)))
     elif available is False:
         interpreter = Interpreter(impl=impl)
-        versions = set(v for v in versions
-                       if not exists(interpreter.binary(v)))
+        versions = set(v for v in versions if not exists(interpreter.binary(v)))
 
     return versions
 
 
-def build_sorted(versions, impl='cpython3'):
+def build_sorted(
+    versions: Iterable[V],
+    impl: str = "cpython3",
+) -> list[Version]:
     """Return sorted list of versions in a build friendly order.
 
     i.e. default version, if among versions, is sorted last.
diff -pruN 6.20251204.1/pybuild 6.20251221/pybuild
--- 6.20251204.1/pybuild	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/pybuild	2025-12-21 21:07:09.000000000 +0000
@@ -24,20 +24,24 @@ import logging
 import argparse
 import re
 import sys
+from enum import StrEnum, auto
 from os import environ, getcwd, makedirs, remove
 from os.path import abspath, exists, isdir, join
 from shutil import rmtree
 from tempfile import mkdtemp
+from typing import Callable, Literal, overload, cast
 
-INTERP_VERSION_RE = re.compile(r'^python(?P<version>3\.\d+)(?P<dbg>-dbg)?$')
-logging.basicConfig(format='%(levelname).1s: pybuild '
-                           '%(module)s:%(lineno)d: %(message)s')
-log = logging.getLogger('dhpython')
+INTERP_VERSION_RE = re.compile(r"^python(?P<version>3\.\d+)(?P<dbg>-dbg)?$")
+logging.basicConfig(
+    format="%(levelname).1s: pybuild " "%(module)s:%(lineno)d: %(message)s"
+)
+log = logging.getLogger("dhpython")
 
 
-def main(cfg):
-    log.debug('cfg: %s', cfg)
+def main(cfg: argparse.Namespace) -> None:
+    log.debug("cfg: %s", cfg)
     from dhpython import build, PKG_PREFIX_MAP
+    from dhpython.build.types import Args, Context
     from dhpython.debhelper import DebHelper, build_options
     from dhpython.version import Version, build_sorted, get_requested_versions
     from dhpython.interpreter import Interpreter
@@ -45,50 +49,54 @@ def main(cfg):
 
     if cfg.list_systems:
         for name, Plugin in sorted(build.plugins.items()):
-            print(name, '\t', Plugin.DESCRIPTION)
+            print(name, "\t", Plugin.DESCRIPTION)
         sys.exit(0)
 
     nocheck = False
-    if 'DEB_BUILD_OPTIONS' in environ:
-        nocheck = 'nocheck' in environ['DEB_BUILD_OPTIONS'].split()
-    if not nocheck and 'DEB_BUILD_PROFILES' in environ:
-        nocheck = 'nocheck' in environ['DEB_BUILD_PROFILES'].split()
+    if "DEB_BUILD_OPTIONS" in environ:
+        nocheck = "nocheck" in environ["DEB_BUILD_OPTIONS"].split()
+    if not nocheck and "DEB_BUILD_PROFILES" in environ:
+        nocheck = "nocheck" in environ["DEB_BUILD_PROFILES"].split()
 
     env = environ.copy()
     # set some defaults in environ to make the build reproducible
-    env.setdefault('LC_ALL', 'C.UTF-8')
-    env.setdefault('CCACHE_DIR', abspath('.pybuild/ccache'))
-    env.setdefault('no_proxy', 'localhost')
-    if 'http_proxy' not in env:
-        env['http_proxy'] = 'http://127.0.0.1:9/'
-    elif not env['http_proxy']:
-        del env['http_proxy']  # some tools don't like empty var.
-    if 'https_proxy' not in env:
-        env['https_proxy'] = 'https://127.0.0.1:9/'
-    elif not env['https_proxy']:
-        del env['https_proxy']  # some tools don't like empty var.
-    if 'DEB_PYTHON_INSTALL_LAYOUT' not in env:
-        env['DEB_PYTHON_INSTALL_LAYOUT'] = 'deb'
+    env.setdefault("LC_ALL", "C.UTF-8")
+    env.setdefault("CCACHE_DIR", abspath(".pybuild/ccache"))
+    env.setdefault("no_proxy", "localhost")
+    if "http_proxy" not in env:
+        env["http_proxy"] = "http://127.0.0.1:9/"
+    elif not env["http_proxy"]:
+        del env["http_proxy"]  # some tools don't like empty var.
+    if "https_proxy" not in env:
+        env["https_proxy"] = "https://127.0.0.1:9/"
+    elif not env["https_proxy"]:
+        del env["https_proxy"]  # some tools don't like empty var.
+    if "DEB_PYTHON_INSTALL_LAYOUT" not in env:
+        env["DEB_PYTHON_INSTALL_LAYOUT"] = "deb"
 
     arch_data = dpkg_architecture()
     if arch_data:
         # Set _PYTHON_HOST_PLATFORM to ensure debugging symbols on, f.e. i386
         # emded a constant name regardless of the 32/64-bit kernel.
         special_cases = {
-            'arm-linux-gnueabihf': 'linux-armv7l',
-            'mips64el-linux-gnuabi64': 'linux-mips64',
-            'mipsel-linux-gnu': 'linux-mips',
-            'powerpc64-linux-gnu': 'linux-ppc64',
-            'powerpc64le-linux-gnu': 'linux-ppc64le',
+            "arm-linux-gnueabihf": "linux-armv7l",
+            "mips64el-linux-gnuabi64": "linux-mips64",
+            "mipsel-linux-gnu": "linux-mips",
+            "powerpc64-linux-gnu": "linux-ppc64",
+            "powerpc64le-linux-gnu": "linux-ppc64le",
         }
-        host_platform = special_cases.get(arch_data['DEB_HOST_MULTIARCH'],
-            '{DEB_HOST_ARCH_OS}-{DEB_HOST_GNU_CPU}'.format(**arch_data))
-        env.setdefault('_PYTHON_HOST_PLATFORM', host_platform)
+        host_platform = special_cases.get(
+            arch_data["DEB_HOST_MULTIARCH"],
+            "{DEB_HOST_ARCH_OS}-{DEB_HOST_GNU_CPU}".format(**arch_data),
+        )
+        env.setdefault("_PYTHON_HOST_PLATFORM", host_platform)
 
-        if arch_data['DEB_BUILD_ARCH'] != arch_data['DEB_HOST_ARCH']:
+        if arch_data["DEB_BUILD_ARCH"] != arch_data["DEB_HOST_ARCH"]:
             # support cross compiling Python 3.X extensions, see #892931
-            env.setdefault('_PYTHON_SYSCONFIGDATA_NAME',
-                           '_sysconfigdata__' + arch_data["DEB_HOST_MULTIARCH"])
+            env.setdefault(
+                "_PYTHON_SYSCONFIGDATA_NAME",
+                "_sysconfigdata__" + arch_data["DEB_HOST_MULTIARCH"],
+            )
 
     # Selected on command line?
     selected_plugin = cfg.system
@@ -97,58 +105,68 @@ def main(cfg):
     if not selected_plugin:
         dh = DebHelper(build_options())
         for build_dep in dh.build_depends:
-            if build_dep.startswith('pybuild-plugin-'):
-                selected_plugin = build_dep.split('-', 2)[2]
+            if build_dep.startswith("pybuild-plugin-"):
+                selected_plugin = build_dep.split("-", 2)[2]
                 break
 
+    context: dict[str, str | dict[str, str]]  # proto-context
     if selected_plugin:
         certainty = 99
         Plugin = build.plugins.get(selected_plugin)
         if not Plugin:
-            log.error('unrecognized build system: %s', selected_plugin)
+            log.error("unrecognized build system: %s", selected_plugin)
             sys.exit(10)
         plugin = Plugin(cfg)
-        context = {'ENV': env, 'args': {}, 'dir': cfg.dir}
+        context = {"ENV": env, "args": {}, "dir": cfg.dir}
         plugin.detect(context)
     else:
-        plugin, certainty, context = None, 0, None
+        plugin, certainty = None, 0
         for Plugin in build.plugins.values():
             try:
                 tmp_plugin = Plugin(cfg)
             except Exception as err:
-                log.warning('cannot initialize %s plugin: %s', Plugin.NAME,
-                         err, exc_info=cfg.verbose)
+                log.warning(
+                    "cannot initialize %s plugin: %s",
+                    Plugin.NAME,
+                    err,
+                    exc_info=cfg.verbose,
+                )
                 continue
-            tmp_context = {'ENV': env, 'args': {}, 'dir': cfg.dir}
+            tmp_context = {"ENV": env, "args": {}, "dir": cfg.dir}
             tmp_certainty = tmp_plugin.detect(tmp_context)
-            log.debug('Plugin %s: certainty %i', Plugin.NAME, tmp_certainty)
+            log.debug("Plugin %s: certainty %i", Plugin.NAME, tmp_certainty)
             if tmp_certainty and tmp_certainty > certainty:
                 plugin, certainty, context = tmp_plugin, tmp_certainty, tmp_context
         del Plugin
         if not plugin:
-            log.error('cannot detect build system, please use --system option'
-                      ' or set PYBUILD_SYSTEM env. variable')
+            log.error(
+                "cannot detect build system, please use --system option"
+                " or set PYBUILD_SYSTEM env. variable"
+            )
             sys.exit(11)
 
     if plugin.SUPPORTED_INTERPRETERS is not True:
         # if versioned interpreter was requested and selected plugin lists
         # versioned ones as supported: extend list of supported interpreters
         # with this interpreter
-        tpls = {i for i in plugin.SUPPORTED_INTERPRETERS if '{version}' in i}
+        tpls = {i for i in plugin.SUPPORTED_INTERPRETERS if "{version}" in i}
         if tpls:
             for ipreter in cfg.interpreter:
                 m = INTERP_VERSION_RE.match(ipreter)
                 if m:
-                    ver = m.group('version')
+                    ver = m.group("version")
                     updated = set(tpl.format(version=ver) for tpl in tpls)
                     if updated:
                         plugin.SUPPORTED_INTERPRETERS.update(updated)
 
     for interpreter in cfg.interpreter:
-        if plugin.SUPPORTED_INTERPRETERS is not True and interpreter not in plugin.SUPPORTED_INTERPRETERS:
-            log.error('interpreter %s not supported by %s', interpreter, plugin)
+        if (
+            plugin.SUPPORTED_INTERPRETERS is not True
+            and interpreter not in plugin.SUPPORTED_INTERPRETERS
+        ):
+            log.error("interpreter %s not supported by %s", interpreter, plugin)
             sys.exit(12)
-    log.debug('detected build system: %s (certainty: %s%%)', plugin.NAME, certainty)
+    log.debug("detected build system: %s (certainty: %s%%)", plugin.NAME, certainty)
 
     if cfg.detect_only:
         if not cfg.really_quiet:
@@ -161,30 +179,53 @@ def main(cfg):
             i = cfg.interpreter[0]
             m = INTERP_VERSION_RE.match(i)
             if m:
-                log.debug('defaulting to version hardcoded in interpreter name')
-                versions = [m.group('version')]
+                log.debug("defaulting to version hardcoded in interpreter name")
+                versions = [m.group("version")]
             else:
                 IMAP = {v: k for k, v in PKG_PREFIX_MAP.items()}
                 if i in IMAP:
-                    versions = build_sorted(get_requested_versions(
-                        IMAP[i], available=True), impl=IMAP[i])
-                if versions and '{version}' not in i:
+                    versions = build_sorted(
+                        get_requested_versions(IMAP[i], available=True), impl=IMAP[i]
+                    )
+                if versions and "{version}" not in i:
                     versions = versions[-1:]  # last one, the default one
         if not versions:  # still no luck
-            log.debug('defaulting to all supported Python 3.X versions')
-            versions = build_sorted(get_requested_versions(
-                'cpython3', available=True), impl='cpython3')
+            log.debug("defaulting to all supported Python 3.X versions")
+            versions = build_sorted(
+                get_requested_versions("cpython3", available=True), impl="cpython3"
+            )
     versions = [Version(v) for v in versions]
 
-    def get_option(name, interpreter=None, version=None, default=None):
+    @overload
+    def get_option(
+        name: str,
+        interpreter: str | None,
+        version: Version | None,
+        default: str,
+    ) -> str: ...
+
+    @overload
+    def get_option(
+        name: str,
+        interpreter: str | None = None,
+        version: Version | None = None,
+        default: None = None,
+    ) -> str | None: ...
+
+    def get_option(
+        name: str,
+        interpreter: str | None = None,
+        version: Version | None = None,
+        default: str | None = None,
+    ) -> str | None:
         if interpreter:
             # try PYBUILD_NAME_python3.3-dbg (or hardcoded interpreter)
-            i = interpreter.format(version=version or '')
+            i = interpreter.format(version=version or "")
             opt = "PYBUILD_{}_{}".format(name.upper(), i)
             if opt in environ:
                 return environ[opt]
             # try PYBUILD_NAME_python3-dbg (if not checked above)
-            if '{version}' in interpreter and version:
+            if "{version}" in interpreter and version:
                 i = interpreter.format(version=version.major)
                 opt = "PYBUILD_{}_{}".format(name.upper(), i)
                 if opt in environ:
@@ -196,157 +237,204 @@ def main(cfg):
         # try command line args
         return getattr(cfg, name, default) or default
 
-    def get_args(context, step, version, interpreter):
+    class Step(StrEnum):
+        CLEAN = auto()
+        CONFIGURE = auto()
+        BUILD = auto()
+        INSTALL = auto()
+        TEST = auto()
+        AUTOPKGTEST = auto()
+
+    def get_args(
+        context: Context,
+        step: Step,
+        version: Version,
+        interpreter: str,
+    ) -> Args:
         i = interpreter.format(version=version)
         ipreter = Interpreter(i)
 
-        home_dir = [ipreter.impl, str(version)]
+        home_dirs = [ipreter.impl, str(version)]
         if ipreter.debug:
-            home_dir.append('dbg')
+            home_dirs.append("dbg")
         if cfg.name:
-            home_dir.append(cfg.name)
+            home_dirs.append(cfg.name)
         if cfg.autopkgtest_only:
-            base_dir = environ.get('AUTOPKGTEST_TMP')
+            base_dir = environ.get("AUTOPKGTEST_TMP")
             if not base_dir:
-                base_dir = mkdtemp(prefix='pybuild-autopkgtest-')
+                base_dir = mkdtemp(prefix="pybuild-autopkgtest-")
         else:
-            base_dir = '.pybuild/{}'
-        home_dir = base_dir.format('_'.join(home_dir))
+            base_dir = ".pybuild/{}"
+        home_dir = base_dir.format("_".join(home_dirs))
 
-        build_dir = get_option('build_dir', interpreter, version,
-                               default=join(home_dir, 'build'))
+        build_dir = get_option(
+            "build_dir", interpreter, version, default=join(home_dir, "build")
+        )
 
-        destdir = context['destdir'].format(version=version, interpreter=i)
+        destdir = context["destdir"].format(version=version, interpreter=i)
         if cfg.name:
             package = ipreter.suggest_pkg_name(cfg.name)
         else:
-            package = 'PYBUILD_NAME_not_set'
-        if cfg.name and destdir.rstrip('/').endswith('debian/tmp'):
+            package = "PYBUILD_NAME_not_set"
+        if cfg.name and destdir.rstrip("/").endswith("debian/tmp"):
             destdir = "debian/{}".format(package)
         destdir = abspath(destdir)
 
-        args = dict(context['args'])
-        args.update({
-            'autopkgtest': cfg.autopkgtest_only,
-            'package': package,
-            'interpreter': ipreter,
-            'version': version,
-            'args': get_option("%s_args" % step, interpreter, version, ''),
-            'dir': abspath(context['dir'].format(version=version, interpreter=i)),
-            'destdir': destdir,
-            'build_dir': abspath(build_dir.format(version=version, interpreter=i)),
-            # versioned dist-packages even for Python 3.X - dh_python3 will fix it later
-            # (and will have a chance to compare files)
-            'install_dir': get_option('install_dir', interpreter, version,
-                                      '/usr/lib/python{version}/dist-packages'
-                                      ).format(version=version, interpreter=i),
-            'home_dir': abspath(home_dir)})
-        env = dict(args.get('ENV', {}))
-        pp = env.get('PYTHONPATH', context['ENV'].get('PYTHONPATH'))
-        pp = pp.split(':') if pp else []
-        if step in {'build', 'test', 'autopkgtest'}:
-            if step in {'test', 'autopkgtest'}:
-                args['test_dir'] = join(args['destdir'], args['install_dir'].lstrip('/'))
-                if args['test_dir'] not in pp:
-                    pp.append(args['test_dir'])
-            if args['build_dir'] not in pp:
-                pp.append(args['build_dir'])
+        args = cast(Args, dict(context["args"]))
+        args.update(
+            {
+                "autopkgtest": cfg.autopkgtest_only,
+                "package": package,
+                "interpreter": ipreter,
+                "version": version,
+                "args": get_option("%s_args" % step, interpreter, version, ""),
+                "dir": abspath(context["dir"].format(version=version, interpreter=i)),
+                "destdir": destdir,
+                "build_dir": abspath(build_dir.format(version=version, interpreter=i)),
+                # versioned dist-packages even for Python 3.X - dh_python3 will fix it later
+                # (and will have a chance to compare files)
+                "install_dir": get_option(
+                    "install_dir",
+                    interpreter,
+                    version,
+                    "/usr/lib/python{version}/dist-packages",
+                ).format(version=version, interpreter=i),
+                "home_dir": abspath(home_dir),
+            }
+        )
+        env = dict(args.get("ENV", {}))
+        pp_str = env.get("PYTHONPATH", context["ENV"].get("PYTHONPATH"))
+        pp = pp_str.split(":") if pp_str else []
+        if step in {"build", "test", "autopkgtest"}:
+            if step in {"test", "autopkgtest"}:
+                args["test_dir"] = join(
+                    args["destdir"], args["install_dir"].lstrip("/")
+                )
+                if args["test_dir"] not in pp:
+                    pp.append(args["test_dir"])
+            if args["build_dir"] not in pp:
+                pp.append(args["build_dir"])
         # cross compilation support for Python 2.x
-        if (version.major == 2 and
-            arch_data.get('DEB_BUILD_ARCH') != arch_data.get('DEB_HOST_ARCH')):
-            pp.insert(0, ('/usr/lib/python{0}/plat-{1}'
-                         ).format(version, arch_data['DEB_HOST_MULTIARCH']))
-        env['PYTHONPATH'] = ':'.join(pp)
+        if version.major == 2 and arch_data.get("DEB_BUILD_ARCH") != arch_data.get(
+            "DEB_HOST_ARCH"
+        ):
+            pp.insert(
+                0,
+                ("/usr/lib/python{0}/plat-{1}").format(
+                    version, arch_data["DEB_HOST_MULTIARCH"]
+                ),
+            )
+        env["PYTHONPATH"] = ":".join(pp)
         # cross compilation support for Python <= 3.8 (see above)
         if version.major == 3:
-            name = '_PYTHON_SYSCONFIGDATA_NAME'
-            value = env.get(name, context['ENV'].get(name, ''))
-            if version << '3.8' and value.startswith('_sysconfigdata_')\
-               and not value.startswith('_sysconfigdata_m'):
+            name = "_PYTHON_SYSCONFIGDATA_NAME"
+            value = env.get(name, context["ENV"].get(name, ""))
+            if (
+                version << "3.8"
+                and value.startswith("_sysconfigdata_")
+                and not value.startswith("_sysconfigdata_m")
+            ):
                 value = env[name] = "_sysconfigdata_m%s" % value[15:]
             # update default from main() for -dbg interpreter
-            if value and ipreter.debug and not value.startswith('_sysconfigdata_d'):
+            if value and ipreter.debug and not value.startswith("_sysconfigdata_d"):
                 env[name] = "_sysconfigdata_d%s" % value[15:]
-        args['ENV'] = env
+        args["ENV"] = env
 
-        if not exists(args['build_dir']):
-            makedirs(args['build_dir'])
+        if not exists(args["build_dir"]):
+            makedirs(args["build_dir"])
 
         return args
 
-    def is_disabled(step, interpreter, version):
+    def is_disabled(step: Step, interpreter: str, version: Version) -> bool:
         i = interpreter
         prefix = "{}/".format(step)
-        disabled = (get_option('disable', i, version) or '').split()
+        disabled = (get_option("disable", i, version) or "").split()
         for item in disabled:
-            if item in (step, '1'):
-                log.debug('disabling %s step for %s %s', step, i, version)
+            if item in (step, "1"):
+                log.debug("disabling %s step for %s %s", step, i, version)
                 return True
             if item.startswith(prefix):
-                disabled.append(item[len(prefix):])
-        if i in disabled or str(version) in disabled or \
-                i.format(version=version) in disabled or \
-                i.format(version=version.major) in disabled:
-            log.debug('disabling %s step for %s %s', step, i, version)
+                disabled.append(item[len(prefix) :])
+        if (
+            i in disabled
+            or str(version) in disabled
+            or i.format(version=version) in disabled
+            or i.format(version=version.major) in disabled
+        ):
+            log.debug("disabling %s step for %s %s", step, i, version)
             return True
         return False
 
-    def run(func, interpreter, version, context):
-        step = func.__func__.__name__
+    def run(
+        func: Callable[[Context, Args], None],
+        interpreter: str,
+        version: Version,
+        context: Context,
+    ) -> None:
+        step = Step(func.__func__.__name__)  # type: ignore
         args = get_args(context, step, version, interpreter)
-        env = dict(context['ENV'])
-        if 'ENV' in args:
-            env.update(args['ENV'])
+        env = dict(context["ENV"])
+        if "ENV" in args:
+            env.update(args["ENV"])
 
-        before_cmd = get_option('before_{}'.format(step), interpreter, version)
+        before_cmd = get_option("before_{}".format(step), interpreter, version)
         if before_cmd:
+            log_file: str | Literal[False]
             if cfg.quiet:
-                log_file = join(args['home_dir'], 'before_{}_cmd.log'.format(step))
+                log_file = join(args["home_dir"], "before_{}_cmd.log".format(step))
             else:
                 log_file = False
             command = before_cmd.format(**args)
             log.info(command)
-            output = execute(command, context['dir'], env, log_file)
-            if output['returncode'] != 0:
-                msg = 'exit code={}: {}'.format(output['returncode'], command)
+            output = execute(command, cwd=context["dir"], env=env, log_output=log_file)
+            if output.returncode != 0:
+                msg = f"exit code={output.returncode}: {command}"
                 raise Exception(msg)
 
-        fpath = join(args['home_dir'], 'testfiles_to_rm_before_install')
-        if step == 'install' and exists(fpath):
+        fpath = join(args["home_dir"], "testfiles_to_rm_before_install")
+        if step == Step.INSTALL and exists(fpath):
             with open(fpath, encoding="UTF-8") as fp:
                 for line in fp:
-                    path = line.strip('\n')
+                    path = line.strip("\n")
                     if exists(path):
                         if isdir(path):
                             rmtree(path)
                         else:
                             remove(path)
             remove(fpath)
-        result = func(context, args)
+        func(context, args)
 
-        after_cmd = get_option('after_{}'.format(step), interpreter, version)
+        after_cmd = get_option("after_{}".format(step), interpreter, version)
         if after_cmd:
             if cfg.quiet:
-                log_file = join(args['home_dir'], 'after_{}_cmd.log'.format(step))
+                log_file = join(args["home_dir"], "after_{}_cmd.log".format(step))
             else:
                 log_file = False
             command = after_cmd.format(**args)
             log.info(command)
-            output = execute(command, context['dir'], env, log_file)
-            if output['returncode'] != 0:
-                msg = 'exit code={}: {}'.format(output['returncode'], command)
+            output = execute(command, cwd=context["dir"], env=env, log_output=log_file)
+            if output.returncode != 0:
+                msg = f"exit code={output.returncode}: {command}"
                 raise Exception(msg)
-        return result
 
-    def move_to_ext_destdir(i, version, context):
+    def move_to_ext_destdir(
+        i: Step,
+        version: Version,
+        context: Context,
+    ) -> None:
         """Move built C extensions from the general destdir to ext_destdir"""
-        args = get_args(context, 'install', version, interpreter)
-        ext_destdir = get_option('ext_destdir', i, version)
+        args = get_args(context, Step.INSTALL, version, interpreter)
+        ext_destdir = get_option("ext_destdir", i, version)
+        ext_pattern = get_option("ext_pattern", i, version)
         if ext_destdir:
-            move_matching_files(args['destdir'], ext_destdir,
-                                get_option('ext_pattern', i, version),
-                                get_option('ext_sub_pattern', i, version),
-                                get_option('ext_sub_repl', i, version))
+            assert ext_pattern
+            move_matching_files(
+                args["destdir"],
+                ext_destdir,
+                ext_pattern,
+                get_option("ext_sub_pattern", i, version),
+                get_option("ext_sub_repl", i, version),
+            )
 
     func = None
     if cfg.clean_only:
@@ -367,33 +455,41 @@ def main(cfg):
     ### one function for each interpreter at a time mode ###
     if func:
         step = func.__func__.__name__
-        if step == 'test' and nocheck:
+        if step == "test" and nocheck:
             sys.exit(0)
         failure = False
         for i in cfg.interpreter:
             ipreter = Interpreter(i.format(version=versions[0]))
             iversions = build_sorted(versions, impl=ipreter.impl)
-            if '{version}' not in i and len(versions) > 1:
-                log.info('limiting Python versions to %s due to missing {version}'
-                         ' in interpreter string', str(versions[-1]))
+            if "{version}" not in i and len(versions) > 1:
+                log.info(
+                    "limiting Python versions to %s due to missing {version}"
+                    " in interpreter string",
+                    str(versions[-1]),
+                )
                 iversions = versions[-1:]  # just the default or closest to default
             for version in iversions:
                 if is_disabled(step, i, version):
                     continue
-                c = dict(context)
-                c['dir'] = get_option('dir', i, version, cfg.dir)
-                c['destdir'] = get_option('destdir', i, version, cfg.destdir)
+                c = cast(Context, dict(context))
+                c["dir"] = get_option("dir", i, version, cfg.dir)
+                c["destdir"] = get_option("destdir", i, version, cfg.destdir)
                 try:
                     run(func, i, version, c)
                 except Exception as err:
-                    log.error('%s: plugin %s failed with: %s',
-                              step, plugin.NAME, err, exc_info=cfg.verbose)
+                    log.error(
+                        "%s: plugin %s failed with: %s",
+                        step,
+                        plugin.NAME,
+                        err,
+                        exc_info=cfg.verbose,
+                    )
                     # try to build/test other interpreters/versions even if
                     # one of them fails to make build logs more verbose:
                     failure = True
-                    if step not in ('build', 'test', 'autopkgtest'):
+                    if step not in (Step.BUILD, Step.TEST, Step.AUTOPKGTEST):
                         sys.exit(13)
-                if step == 'install':
+                if step == Step.INSTALL:
                     move_to_ext_destdir(i, version, c)
         if failure:
             # exit with a non-zero return code if at least one build/test failed
@@ -402,178 +498,318 @@ def main(cfg):
 
     ### all functions for interpreters in batches mode ###
     try:
-        context_map = {}
+        context_map: dict[tuple[str, Version], Context] = {}
         for i in cfg.interpreter:
             ipreter = Interpreter(i.format(version=versions[0]))
             iversions = build_sorted(versions, impl=ipreter.impl)
-            if '{version}' not in i and len(versions) > 1:
-                log.info('limiting Python versions to %s due to missing {version}'
-                         ' in interpreter string', str(versions[-1]))
+            if "{version}" not in i and len(versions) > 1:
+                log.info(
+                    "limiting Python versions to %s due to missing {version}"
+                    " in interpreter string",
+                    str(versions[-1]),
+                )
                 iversions = versions[-1:]  # just the default or closest to default
             for version in iversions:
                 key = (i, version)
                 if key in context_map:
                     c = context_map[key]
                 else:
-                    c = dict(context)
-                    c['dir'] = get_option('dir', i, version, cfg.dir)
-                    c['destdir'] = get_option('destdir', i, version, cfg.destdir)
+                    c = cast(Context, dict(context))
+                    c["dir"] = get_option("dir", i, version, cfg.dir)
+                    c["destdir"] = get_option("destdir", i, version, cfg.destdir)
                     context_map[key] = c
 
-                if not is_disabled('clean', i, version):
+                if not is_disabled(Step.CLEAN, i, version):
                     run(plugin.clean, i, version, c)
-                if not is_disabled('configure', i, version):
+                if not is_disabled(Step.CONFIGURE, i, version):
                     run(plugin.configure, i, version, c)
-                if not is_disabled('build', i, version):
+                if not is_disabled(Step.BUILD, i, version):
                     run(plugin.build, i, version, c)
-                if not is_disabled('install', i, version):
+                if not is_disabled(Step.INSTALL, i, version):
                     run(plugin.install, i, version, c)
                     move_to_ext_destdir(i, version, c)
-                if not nocheck and not is_disabled('test', i, version):
+                if not nocheck and not is_disabled(Step.TEST, i, version):
                     run(plugin.test, i, version, c)
     except Exception as err:
-        log.error('plugin %s failed: %s', plugin.NAME, err,
-                  exc_info=cfg.verbose)
+        log.error("plugin %s failed: %s", plugin.NAME, err, exc_info=cfg.verbose)
         sys.exit(14)
 
 
-def parse_args(argv):
-    usage = '%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]'
+def parse_args(argv: list[str]) -> argparse.Namespace:
+    usage = "%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]"
     parser = argparse.ArgumentParser(usage=usage)
-    parser.add_argument('-v', '--verbose', action='store_true',
-                        default=environ.get('PYBUILD_VERBOSE') == '1',
-                        help='turn verbose mode on')
-    parser.add_argument('-q', '--quiet', action='store_true',
-                        default=environ.get('PYBUILD_QUIET') == '1',
-                        help='doesn\'t show external command\'s output')
-    parser.add_argument('-qq', '--really-quiet', action='store_true',
-                        default=environ.get('PYBUILD_RQUIET') == '1',
-                        help='be quiet')
-    parser.add_argument('--version', action='version', version='%(prog)s DEVELV')
-
-    action = parser.add_argument_group('ACTION', '''The default is to build,
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        default=environ.get("PYBUILD_VERBOSE") == "1",
+        help="turn verbose mode on",
+    )
+    parser.add_argument(
+        "-q",
+        "--quiet",
+        action="store_true",
+        default=environ.get("PYBUILD_QUIET") == "1",
+        help="doesn't show external command's output",
+    )
+    parser.add_argument(
+        "-qq",
+        "--really-quiet",
+        action="store_true",
+        default=environ.get("PYBUILD_RQUIET") == "1",
+        help="be quiet",
+    )
+    parser.add_argument("--version", action="version", version="%(prog)s DEVELV")
+
+    action = parser.add_argument_group(
+        "ACTION",
+        """The default is to build,
         install and test the library using detected build system version by
         version. Selecting one of following actions, will invoke given action
         for all versions - one by one - which (contrary to the default action)
-        in some build systems can overwrite previous results.''')
-    action.add_argument('--detect', action='store_true', dest='detect_only',
-                        help='return the name of detected build system')
-    action.add_argument('--clean', action='store_true', dest='clean_only',
-                        help='clean files using auto-detected build system specific methods')
-    action.add_argument('--configure', action='store_true', dest='configure_only',
-                        help='invoke configure step for all requested Python versions')
-    action.add_argument('--build', action='store_true', dest='build_only',
-                        help='invoke build step for all requested Python versions')
-    action.add_argument('--install', action='store_true', dest='install_only',
-                        help='invoke install step for all requested Python versions')
-    action.add_argument('--test', action='store_true', dest='test_only',
-                        help='invoke tests for auto-detected build system')
-    action.add_argument('--autopkgtest', action='store_true', dest='autopkgtest_only',
-                        help='invoke autopkgtests for auto-detected build system')
-    action.add_argument('--list-systems', action='store_true',
-                        help='list available build systems and exit')
-    action.add_argument('--print', action='append', dest='print_args',
-                        help="print pybuild's internal parameters")
-
-    arguments = parser.add_argument_group('BUILD SYSTEM ARGS', '''
+        in some build systems can overwrite previous results.""",
+    )
+    action.add_argument(
+        "--detect",
+        action="store_true",
+        dest="detect_only",
+        help="return the name of detected build system",
+    )
+    action.add_argument(
+        "--clean",
+        action="store_true",
+        dest="clean_only",
+        help="clean files using auto-detected build system specific methods",
+    )
+    action.add_argument(
+        "--configure",
+        action="store_true",
+        dest="configure_only",
+        help="invoke configure step for all requested Python versions",
+    )
+    action.add_argument(
+        "--build",
+        action="store_true",
+        dest="build_only",
+        help="invoke build step for all requested Python versions",
+    )
+    action.add_argument(
+        "--install",
+        action="store_true",
+        dest="install_only",
+        help="invoke install step for all requested Python versions",
+    )
+    action.add_argument(
+        "--test",
+        action="store_true",
+        dest="test_only",
+        help="invoke tests for auto-detected build system",
+    )
+    action.add_argument(
+        "--autopkgtest",
+        action="store_true",
+        dest="autopkgtest_only",
+        help="invoke autopkgtests for auto-detected build system",
+    )
+    action.add_argument(
+        "--list-systems",
+        action="store_true",
+        help="list available build systems and exit",
+    )
+    action.add_argument(
+        "--print",
+        action="append",
+        dest="print_args",
+        help="print pybuild's internal parameters",
+    )
+
+    arguments = parser.add_argument_group(
+        "BUILD SYSTEM ARGS",
+        """
         Additional arguments passed to the build system.
-        --system=custom requires complete command.''')
-    arguments.add_argument('--before-clean', metavar='CMD',
-                           help='invoked before the clean command')
-    arguments.add_argument('--clean-args', metavar='ARGS')
-    arguments.add_argument('--after-clean', metavar='CMD',
-                           help='invoked after the clean command')
-
-    arguments.add_argument('--before-configure', metavar='CMD',
-                           help='invoked before the configure command')
-    arguments.add_argument('--configure-args', metavar='ARGS')
-    arguments.add_argument('--after-configure', metavar='CMD',
-                           help='invoked after the configure command')
-
-    arguments.add_argument('--before-build', metavar='CMD',
-                           help='invoked before the build command')
-    arguments.add_argument('--build-args', metavar='ARGS')
-    arguments.add_argument('--after-build', metavar='CMD',
-                           help='invoked after the build command')
-
-    arguments.add_argument('--before-install', metavar='CMD',
-                           help='invoked before the install command')
-    arguments.add_argument('--install-args', metavar='ARGS')
-    arguments.add_argument('--after-install', metavar='CMD',
-                           help='invoked after the install command')
-
-    arguments.add_argument('--before-test', metavar='CMD',
-                           help='invoked before the test command')
-    arguments.add_argument('--test-args', metavar='ARGS')
-    arguments.add_argument('--after-test', metavar='CMD',
-                           help='invoked after the test command')
-
-    tests = parser.add_argument_group('TESTS', '''\
-        unittest\'s discover is used by default (if available)''')
-    tests.add_argument('--test-nose', action='store_true',
-                       default=environ.get('PYBUILD_TEST_NOSE') == '1',
-                       help='use nose module in --test step')
-    tests.add_argument('--test-nose2', action='store_true',
-                       default=environ.get('PYBUILD_TEST_NOSE2') == '1',
-                       help='use nose2 module in --test step')
-    tests.add_argument('--test-pytest', action='store_true',
-                       default=environ.get('PYBUILD_TEST_PYTEST') == '1',
-                       help='use pytest module in --test step')
-    tests.add_argument('--test-tox', action='store_true',
-                       default=environ.get('PYBUILD_TEST_TOX') == '1',
-                       help='use tox in --test step')
-    tests.add_argument('--test-stestr', action='store_true',
-                       default=environ.get('PYBUILD_TEST_STESTR') == '1',
-                       help='use stestr in --test step')
-    tests.add_argument('--test-custom', action='store_true',
-                       default=environ.get('PYBUILD_TEST_CUSTOM') == '1',
-                       help='use custom command in --test step')
-
-    dirs = parser.add_argument_group('DIRECTORIES')
-    dirs.add_argument('-d', '--dir', action='store', metavar='DIR',
-                      default=environ.get('PYBUILD_DIR', getcwd()),
-                      help='source files directory - base for other relative dirs [default: CWD]')
-    dirs.add_argument('--dest-dir', action='store', metavar='DIR', dest='destdir',
-                      default=environ.get('DESTDIR', 'debian/tmp'),
-                      help='destination directory [default: debian/tmp]')
-    dirs.add_argument('--ext-dest-dir', action='store', metavar='DIR', dest='ext_destdir',
-                      default=environ.get('PYBUILD_EXT_DESTDIR'),
-                      help='destination directory for .so files')
-    dirs.add_argument('--ext-pattern', action='store', metavar='PATTERN',
-                      default=environ.get('PYBUILD_EXT_PATTERN', r'\.so(\.[^/]*)?$'),
-                      help='regular expression for files that should be moved'
-                      ' if --ext-dest-dir is set [default: .so files]')
-    dirs.add_argument('--ext-sub-pattern', action='store', metavar='PATTERN',
-                      default=environ.get('PYBUILD_EXT_SUB_PATTERN'),
-                      help='pattern to change --ext-pattern\'s filename or path')
-    dirs.add_argument('--ext-sub-repl', action='store', metavar='PATTERN',
-                      default=environ.get('PYBUILD_EXT_SUB_REPL'),
-                      help='replacement for match from --ext-sub-pattern,'
-                      ' empty string by default')
-    dirs.add_argument('--install-dir', action='store', metavar='DIR',
-                      help='installation directory [default: .../dist-packages]')
-    dirs.add_argument('--name', action='store',
-                      default=environ.get('PYBUILD_NAME'),
-                      help='use this name to guess destination directories')
-
-    limit = parser.add_argument_group('LIMITATIONS')
-    limit.add_argument('-s', '--system',
-                       default=environ.get('PYBUILD_SYSTEM'),
-                       help='select a build system [default: auto-detection]')
-    limit.add_argument('-p', '--pyver', action='append', dest='versions',
-                       help='''build for Python VERSION.
+        --system=custom requires complete command.""",
+    )
+    arguments.add_argument(
+        "--before-clean", metavar="CMD", help="invoked before the clean command"
+    )
+    arguments.add_argument("--clean-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-clean", metavar="CMD", help="invoked after the clean command"
+    )
+
+    arguments.add_argument(
+        "--before-configure", metavar="CMD", help="invoked before the configure command"
+    )
+    arguments.add_argument("--configure-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-configure", metavar="CMD", help="invoked after the configure command"
+    )
+
+    arguments.add_argument(
+        "--before-build", metavar="CMD", help="invoked before the build command"
+    )
+    arguments.add_argument("--build-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-build", metavar="CMD", help="invoked after the build command"
+    )
+
+    arguments.add_argument(
+        "--before-install", metavar="CMD", help="invoked before the install command"
+    )
+    arguments.add_argument("--install-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-install", metavar="CMD", help="invoked after the install command"
+    )
+
+    arguments.add_argument(
+        "--before-test", metavar="CMD", help="invoked before the test command"
+    )
+    arguments.add_argument("--test-args", metavar="ARGS")
+    arguments.add_argument(
+        "--after-test", metavar="CMD", help="invoked after the test command"
+    )
+
+    tests = parser.add_argument_group(
+        "TESTS",
+        """\
+        unittest\'s discover is used by default (if available)""",
+    )
+    tests.add_argument(
+        "--test-nose",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_NOSE") == "1",
+        help="use nose module in --test step",
+    )
+    tests.add_argument(
+        "--test-nose2",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_NOSE2") == "1",
+        help="use nose2 module in --test step",
+    )
+    tests.add_argument(
+        "--test-pytest",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_PYTEST") == "1",
+        help="use pytest module in --test step",
+    )
+    tests.add_argument(
+        "--test-tox",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_TOX") == "1",
+        help="use tox in --test step",
+    )
+    tests.add_argument(
+        "--test-stestr",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_STESTR") == "1",
+        help="use stestr in --test step",
+    )
+    tests.add_argument(
+        "--test-unittest",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_UNITTEST") == "1",
+        help="use unittest in --test step",
+    )
+    tests.add_argument(
+        "--test-custom",
+        action="store_true",
+        default=environ.get("PYBUILD_TEST_CUSTOM") == "1",
+        help="use custom command in --test step",
+    )
+
+    dirs = parser.add_argument_group("DIRECTORIES")
+    dirs.add_argument(
+        "-d",
+        "--dir",
+        action="store",
+        metavar="DIR",
+        default=environ.get("PYBUILD_DIR", getcwd()),
+        help="source files directory - base for other relative dirs [default: CWD]",
+    )
+    dirs.add_argument(
+        "--dest-dir",
+        action="store",
+        metavar="DIR",
+        dest="destdir",
+        default=environ.get("DESTDIR", "debian/tmp"),
+        help="destination directory [default: debian/tmp]",
+    )
+    dirs.add_argument(
+        "--ext-dest-dir",
+        action="store",
+        metavar="DIR",
+        dest="ext_destdir",
+        default=environ.get("PYBUILD_EXT_DESTDIR"),
+        help="destination directory for .so files",
+    )
+    dirs.add_argument(
+        "--ext-pattern",
+        action="store",
+        metavar="PATTERN",
+        default=environ.get("PYBUILD_EXT_PATTERN", r"\.so(\.[^/]*)?$"),
+        help="regular expression for files that should be moved"
+        " if --ext-dest-dir is set [default: .so files]",
+    )
+    dirs.add_argument(
+        "--ext-sub-pattern",
+        action="store",
+        metavar="PATTERN",
+        default=environ.get("PYBUILD_EXT_SUB_PATTERN"),
+        help="pattern to change --ext-pattern's filename or path",
+    )
+    dirs.add_argument(
+        "--ext-sub-repl",
+        action="store",
+        metavar="PATTERN",
+        default=environ.get("PYBUILD_EXT_SUB_REPL"),
+        help="replacement for match from --ext-sub-pattern," " empty string by default",
+    )
+    dirs.add_argument(
+        "--install-dir",
+        action="store",
+        metavar="DIR",
+        help="installation directory [default: .../dist-packages]",
+    )
+    dirs.add_argument(
+        "--name",
+        action="store",
+        default=environ.get("PYBUILD_NAME"),
+        help="use this name to guess destination directories",
+    )
+
+    limit = parser.add_argument_group("LIMITATIONS")
+    limit.add_argument(
+        "-s",
+        "--system",
+        default=environ.get("PYBUILD_SYSTEM"),
+        help="select a build system [default: auto-detection]",
+    )
+    limit.add_argument(
+        "-p",
+        "--pyver",
+        action="append",
+        dest="versions",
+        help="""build for Python VERSION.
                                This option can be used multiple times
-                               [default: all supported Python 3.X versions]''')
-    limit.add_argument('-i', '--interpreter', action='append',
-                       help='change interpreter [default: python{version}]')
-    limit.add_argument('--disable', metavar='ITEMS',
-                       help='disable action, interpreter or version')
+                               [default: all supported Python 3.X versions]""",
+    )
+    limit.add_argument(
+        "-i",
+        "--interpreter",
+        action="append",
+        help="change interpreter [default: python{version}]",
+    )
+    limit.add_argument(
+        "--disable", metavar="ITEMS", help="disable action, interpreter or version"
+    )
 
     args = parser.parse_args(argv)
     if not args.interpreter:
-        args.interpreter = environ.get('PYBUILD_INTERPRETERS', 'python{version}').split()
+        args.interpreter = environ.get(
+            "PYBUILD_INTERPRETERS", "python{version}"
+        ).split()
     if not args.versions:
-        args.versions = environ.get('PYBUILD_VERSIONS', '').split()
+        args.versions = environ.get("PYBUILD_VERSIONS", "").split()
     else:
         # add support for -p `pyversions -rv`
         versions = []
@@ -581,8 +817,15 @@ def parse_args(argv):
             versions.extend(version.split())
         args.versions = versions
 
-    if args.test_nose or args.test_nose2 or args.test_pytest or args.test_tox\
-       or args.test_stestr or args.test_custom or args.system == 'custom':
+    if (
+        args.test_nose
+        or args.test_nose2
+        or args.test_pytest
+        or args.test_tox
+        or args.test_stestr
+        or args.test_custom
+        or args.system == "custom"
+    ):
         args.custom_tests = True
     else:
         args.custom_tests = False
@@ -590,7 +833,7 @@ def parse_args(argv):
     return args
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     cfg = parse_args(sys.argv[1:])
     if cfg.really_quiet:
         cfg.quiet = True
@@ -599,7 +842,7 @@ if __name__ == '__main__':
         log.setLevel(logging.DEBUG)
     else:
         log.setLevel(logging.INFO)
-    log.debug('version: DEVELV')
+    log.debug("version: DEVELV")
     log.debug(sys.argv)
     main(cfg)
     # let dh clean the .pybuild dir
diff -pruN 6.20251204.1/pybuild.rst 6.20251221/pybuild.rst
--- 6.20251204.1/pybuild.rst	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/pybuild.rst	2025-12-21 21:07:09.000000000 +0000
@@ -87,7 +87,9 @@ ACTION
 
 TESTS
 -----
-    unittest's discover from standard library is used in test step by default.
+    unittest's discover from standard library is used in test step by
+    default. But if no tests are found, this is ignored. Use
+    `--test-unittest` to require tests to be run.
 
     --test-nose
         use nose module in test step, remember to add python3-nose to
@@ -104,6 +106,9 @@ TESTS
     --test-stestr
         use stestr command in test step, remember to add python3-stestr
         to Build-Depends.
+    --test-unittest
+        use unittest discover in the test step, requiring at least 1
+        test to be discovered.
     --test-custom
         use a custom command in the test step. The full test command is
         then specified with `--test-args` or by setting the
diff -pruN 6.20251204.1/pydist/cpython3_fallback 6.20251221/pydist/cpython3_fallback
--- 6.20251204.1/pydist/cpython3_fallback	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/pydist/cpython3_fallback	2025-12-21 21:07:09.000000000 +0000
@@ -890,7 +890,6 @@ blake3 python3-blake3
 blazar python3-blazar
 blazar-dashboard python3-blazar-dashboard
 blazar-nova python3-blazarnova
-blazar-tempest-plugin blazar-tempest-plugin
 bleach python3-bleach
 bleak python3-bleak
 bleak-esphome python3-bleak-esphome
@@ -4926,7 +4925,7 @@ rapid-photo-downloader rapid-photo-downl
 rapidfuzz python3-rapidfuzz
 rapt-ble python3-rapt-ble
 rarfile python3-rarfile
-raritan python3-raritan-json-rpc
+raritan-json-rpc python3-raritan-json-rpc
 rasterio python3-rasterio
 ratelimit python3-ratelimit
 ratelimiter python3-ratelimiter
@@ -5011,7 +5010,7 @@ requests-mock python3-requests-mock
 requests-ntlm python3-requests-ntlm
 requests-oauthlib python3-requests-oauthlib
 requests-toolbelt python3-requests-toolbelt
-requests-unixsocket2 python3-requests-unixsocket
+requests-unixsocket python3-requests-unixsocket
 requestsexceptions python3-requestsexceptions
 requirements-detector python3-requirements-detector
 requirements-parser python3-requirements
@@ -5835,7 +5834,6 @@ traittypes python3-traittypes
 trame python3-trame
 trame-client python3-trame-client
 trame-common python3-trame-common
-trame-server python3-trame-server
 transaction python3-transaction
 transforms3d python3-transforms3d
 transip python3-transip
diff -pruN 6.20251204.1/pydist/generate_fallback_list.py 6.20251221/pydist/generate_fallback_list.py
--- 6.20251204.1/pydist/generate_fallback_list.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/pydist/generate_fallback_list.py	2025-12-21 21:07:09.000000000 +0000
@@ -21,6 +21,7 @@
 
 import re
 import sys
+
 try:
     from distro_info import DistroInfo  # python3-distro-info package
 except ImportError:
@@ -30,30 +31,31 @@ from os import chdir, mkdir
 from os.path import dirname, exists, isdir, join, split
 from urllib.request import urlopen
 
-if '--ubuntu' in sys.argv and DistroInfo:
+if "--ubuntu" in sys.argv and DistroInfo:
     SOURCES = [
-        'http://archive.ubuntu.com/ubuntu/dists/%s/Contents-amd64.gz' %
-        DistroInfo('ubuntu').devel(),
+        "http://archive.ubuntu.com/ubuntu/dists/%s/Contents-amd64.gz"
+        % DistroInfo("ubuntu").devel(),
     ]
 else:
     SOURCES = [
-        'http://ftp.debian.org/debian/dists/unstable/main/Contents-all.gz',
-        'http://ftp.debian.org/debian/dists/unstable/main/Contents-amd64.gz',
+        "http://ftp.debian.org/debian/dists/unstable/main/Contents-all.gz",
+        "http://ftp.debian.org/debian/dists/unstable/main/Contents-amd64.gz",
     ]
 
 IGNORED_PKGS = set()
 OVERRIDES = {
-    'cpython3': {
-        'argparse': 'python3 (>= 3.2)',
-        'cython': 'cython3',
-        'pil': 'python3-pil',
-        'pillow': 'python3-pil',
-        'pylint': 'pylint',
-        'pyside6': None,
+    "cpython3": {
+        "argparse": "python3 (>= 3.2)",
+        "cython": "cython3",
+        "pil": "python3-pil",
+        "pillow": "python3-pil",
+        "pylint": "pylint",
+        "pyside6": None,
     },
 }
 
-public_egg = re.compile(r'''
+public_egg = re.compile(
+    r"""
     /usr/
     (
         (?P<cpython3>
@@ -61,38 +63,38 @@ public_egg = re.compile(r'''
         )
     )
     /[^/]*\.(dist|egg)-info
-''', re.VERBOSE).match
+""",
+    re.VERBOSE,
+).match
 
-skip_sensible_names = True if '--skip-sensible-names' in sys.argv else False
+skip_sensible_names = True if "--skip-sensible-names" in sys.argv else False
 
 chdir(dirname(__file__))
-if isdir('../dhpython'):
-    sys.path.append('..')
+if isdir("../dhpython"):
+    sys.path.append("..")
 else:
-    sys.path.append('/usr/share/dh-python/dhpython/')
+    sys.path.append("/usr/share/dh-python/dhpython/")
 from dhpython.pydist import normalize_name, sensible_pname
 
-data = ''
-if not isdir('cache'):
-    mkdir('cache')
+data = ""
+if not isdir("cache"):
+    mkdir("cache")
 for source in SOURCES:
-    cache_fpath = join('cache', split(source)[-1])
+    cache_fpath = join("cache", split(source)[-1])
     if not exists(cache_fpath):
         with urlopen(source) as fp:
             source_data = fp.read()
-        with open(cache_fpath, 'wb') as fp:
+        with open(cache_fpath, "wb") as fp:
             fp.write(source_data)
     else:
-        with open(cache_fpath, 'rb') as fp:
+        with open(cache_fpath, "rb") as fp:
             source_data = fp.read()
     try:
-        data += str(decompress(source_data), encoding='UTF-8')
+        data += str(decompress(source_data), encoding="UTF-8")
     except UnicodeDecodeError as e:  # Ubuntu
-        data += str(decompress(source_data), encoding='ISO-8859-15')
+        data += str(decompress(source_data), encoding="ISO-8859-15")
 
-result = {
-    'cpython3': {}
-}
+result = {"cpython3": {}}
 
 for line in data.splitlines():
     try:
@@ -100,22 +102,24 @@ for line in data.splitlines():
     except ValueError:
         # NOTE(jamespage) some lines in Ubuntu are not parseable.
         continue
-    path = '/' + path.rstrip()
-    section, pkg_name = desc.rsplit('/', 1)
+    path = "/" + path.rstrip()
+    section, pkg_name = desc.rsplit("/", 1)
     if pkg_name in IGNORED_PKGS:
         continue
     match = public_egg(path)
     if match:
-        dist_name = [i.split('-', 1)[0] for i in path.split('/')
-                     if i.endswith(('.egg-info', '.dist-info'))][0]
-        if dist_name.endswith('.egg'):
+        dist_name = [
+            i.split("-", 1)[0]
+            for i in path.split("/")
+            if i.endswith((".egg-info", ".dist-info"))
+        ][0]
+        if dist_name.endswith(".egg"):
             dist_name = dist_name[:-4]
         dist_name = normalize_name(dist_name)
 
         impl = next(key for key, value in match.groupdict().items() if value)
 
-        if skip_sensible_names and\
-                sensible_pname(impl, dist_name) == pkg_name:
+        if skip_sensible_names and sensible_pname(impl, dist_name) == pkg_name:
             continue
 
         if impl not in result:
@@ -126,13 +130,15 @@ for line in data.splitlines():
             processed[dist_name] = pkg_name
 
 for impl, details in result.items():
-    with open('{}_fallback'.format(impl), 'w') as fp:
+    with open("{}_fallback".format(impl), "w") as fp:
         overrides = OVERRIDES[impl]
         lines = []
         for egg, value in overrides.items():
             if value:
-                lines.append('{} {}\n'.format(egg, value))
+                lines.append("{} {}\n".format(egg, value))
         lines.extend(
-            '{} {}\n'.format(egg, pkg) for egg, pkg in details.items() if egg not in overrides
+            "{} {}\n".format(egg, pkg)
+            for egg, pkg in details.items()
+            if egg not in overrides
         )
         fp.writelines(sorted(lines))
diff -pruN 6.20251204.1/tests/common.py 6.20251221/tests/common.py
--- 6.20251204.1/tests/common.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/common.py	2025-12-21 21:07:09.000000000 +0000
@@ -1,17 +1,35 @@
+from typing import Any
+
+from dhpython.version import VersionRange
+
+
 class FakeOptions:
-    def __init__(self, **kwargs):
+    accept_upstream_versions: bool = False
+    depends: list[str]
+    depends_section: list[str]
+    guess_deps: bool = True
+    no_ext_rename: bool = False
+    recommends: list[str]
+    recommends_section: list[str]
+    remaining_packages: bool = False
+    requires: list[str]
+    suggests: list[str]
+    suggests_section: list[str]
+    vrange: VersionRange | None = None
+
+    def __init__(self, **kwargs: Any) -> None:
         opts = {
-            'depends': (),
-            'depends_section': (),
-            'guess_deps': False,
-            'no_ext_rename': False,
-            'recommends': (),
-            'recommends_section': (),
-            'requires': (),
-            'suggests': (),
-            'suggests_section': (),
-            'vrange': None,
-            'accept_upstream_versions': False,
+            "depends": (),
+            "depends_section": (),
+            "guess_deps": False,
+            "no_ext_rename": False,
+            "recommends": (),
+            "recommends_section": (),
+            "requires": (),
+            "suggests": (),
+            "suggests_section": (),
+            "vrange": None,
+            "accept_upstream_versions": False,
         }
         opts.update(kwargs)
         for k, v in opts.items():
diff -pruN 6.20251204.1/tests/test_debhelper.py 6.20251221/tests/test_debhelper.py
--- 6.20251204.1/tests/test_debhelper.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_debhelper.py	2025-12-21 21:07:09.000000000 +0000
@@ -1,20 +1,21 @@
-from tempfile import TemporaryDirectory
-import unittest
 import os
+import unittest
+from tempfile import TemporaryDirectory
+from typing import Any
 
-from dhpython.debhelper import DebHelper, build_options
+from dhpython.debhelper import DebHelper, build_options, Options
 
 
 class DebHelperTestCase(unittest.TestCase):
-    impl = 'cpython3'
-    control = []
-    options = {}
+    impl = "cpython3"
+    control: list[str] = []
+    options: dict[str, Any] = {}
     parse_control = True
 
-    def build_options(self):
+    def build_options(self) -> Options:
         return build_options(**self.options)
 
-    def setUp(self):
+    def setUp(self) -> None:
         self.tempdir = TemporaryDirectory()  # pylint: disable=consider-using-with
         self.addCleanup(self.tempdir.cleanup)
 
@@ -22,143 +23,150 @@ class DebHelperTestCase(unittest.TestCas
         os.chdir(self.tempdir.name)
         self.addCleanup(os.chdir, old_wd)
 
-        os.mkdir('debian')
-        with open('debian/control', 'w', encoding="UTF-8") as f:
-            f.write('\n'.join(self.control))
+        os.mkdir("debian")
+        with open("debian/control", "w", encoding="UTF-8") as f:
+            f.write("\n".join(self.control))
         if self.parse_control:
             self.dh = DebHelper(self.build_options(), impl=self.impl)
 
 
 CONTROL = [
-    'Source: foo-src',
-    'Build-Depends: python3-all,',
-    ' python-all',
-    '\t, bar (<< 2) [amd64],',
-    ' baz (>= 1.0)',
-    'X-Python3-Version: >= 3.1, << 3.10',
-    '',
-    'Architecture: all',
-    'Package: python3-foo',
-    'Depends: ${python3:Depends}',
-    '',
-    'Package: python3-foo-ext',
-    'Architecture: any',
-    'Depends: ${python3:Depends}, '
-    '# COMMENT',
-    ' ${shlibs:Depends},',
-    '',
-    'Package: python-foo',
-    'Architecture: all',
-    'Depends: ${python:Depends}',
-    '',
-    '',
-    'Package: foo',
-    'Architecture: all',
-    'Depends: ${python3:Depends}',
-    '',
-    '',
-    'Package: recfoo',
-    'Architecture: all',
-    'Recommends: ${python3:Depends}',
-    '',
-    '',
+    "Source: foo-src",
+    "Build-Depends: python3-all,",
+    " python-all",
+    "\t, bar (<< 2) [amd64],",
+    " baz (>= 1.0)",
+    "X-Python3-Version: >= 3.1, << 3.10",
+    "",
+    "Architecture: all",
+    "Package: python3-foo",
+    "Depends: ${python3:Depends}",
+    "",
+    "Package: python3-foo-ext",
+    "Architecture: any",
+    "Depends: ${python3:Depends}, " "# COMMENT",
+    " ${shlibs:Depends},",
+    "",
+    "Package: python-foo",
+    "Architecture: all",
+    "Depends: ${python:Depends}",
+    "",
+    "",
+    "Package: foo",
+    "Architecture: all",
+    "Depends: ${python3:Depends}",
+    "",
+    "",
+    "Package: recfoo",
+    "Architecture: all",
+    "Recommends: ${python3:Depends}",
+    "",
+    "",
 ]
 
+
 class TestControlBlockParsing(DebHelperTestCase):
     control = CONTROL
 
-    def test_parses_source(self):
-        self.assertEqual(self.dh.source_name, 'foo-src')
+    def test_parses_source(self) -> None:
+        self.assertEqual(self.dh.source_name, "foo-src")
 
-    def test_parses_build_depends(self):
-        self.assertEqual(self.dh.build_depends, {
-            'python3-all': {None: None},
-            'python-all': {None: None},
-            'bar': {'amd64': '<< 2'},
-            'baz': {None: '>= 1.0'},
-        })
-
-    def test_parses_XPV(self):
-        self.assertEqual(self.dh.python_version, '>= 3.1, << 3.10')
-
-    def test_parses_packages(self):
-        self.assertEqual(list(self.dh.packages.keys()),
-                         ['python3-foo', 'python3-foo-ext', 'foo', 'recfoo'])
+    def test_parses_build_depends(self) -> None:
+        self.assertEqual(
+            self.dh.build_depends,
+            {
+                "python3-all": {None: None},
+                "python-all": {None: None},
+                "bar": {"amd64": "<< 2"},
+                "baz": {None: ">= 1.0"},
+            },
+        )
+
+    def test_parses_XPV(self) -> None:
+        self.assertEqual(self.dh.python_version, ">= 3.1, << 3.10")
+
+    def test_parses_packages(self) -> None:
+        self.assertEqual(
+            list(self.dh.packages.keys()),
+            ["python3-foo", "python3-foo-ext", "foo", "recfoo"],
+        )
 
-    def test_parses_arch(self):
-        self.assertEqual(self.dh.packages['python3-foo-ext']['arch'], 'any')
+    def test_parses_arch(self) -> None:
+        self.assertEqual(self.dh.packages["python3-foo-ext"]["arch"], "any")
 
-    def test_parses_arch_all(self):
-        self.assertEqual(self.dh.packages['python3-foo']['arch'], 'all')
+    def test_parses_arch_all(self) -> None:
+        self.assertEqual(self.dh.packages["python3-foo"]["arch"], "all")
 
 
 class TestControlSkipIndep(DebHelperTestCase):
     control = CONTROL
     options = {
-        'arch': True,
+        "arch": True,
     }
 
-    def test_skip_indep(self):
-        self.assertEqual(list(self.dh.packages.keys()), ['python3-foo-ext'])
+    def test_skip_indep(self) -> None:
+        self.assertEqual(list(self.dh.packages.keys()), ["python3-foo-ext"])
 
 
 class TestControlSkipArch(DebHelperTestCase):
     control = CONTROL
     options = {
-        'arch': False,
+        "arch": False,
     }
 
-    def test_skip_arch(self):
-        self.assertEqual(list(self.dh.packages.keys()),
-                         ['python3-foo', 'foo', 'recfoo'])
+    def test_skip_arch(self) -> None:
+        self.assertEqual(
+            list(self.dh.packages.keys()), ["python3-foo", "foo", "recfoo"]
+        )
 
 
 class TestControlSinglePkg(DebHelperTestCase):
     control = CONTROL
     options = {
-        'package': ['python3-foo'],
+        "package": ["python3-foo"],
     }
 
-    def test_parses_packages(self):
-        self.assertEqual(list(self.dh.packages.keys()), ['python3-foo'])
+    def test_parses_packages(self) -> None:
+        self.assertEqual(list(self.dh.packages.keys()), ["python3-foo"])
 
 
 class TestControlSkipSinglePkg(DebHelperTestCase):
     control = CONTROL
     options = {
-        'no_package': ['python3-foo'],
+        "no_package": ["python3-foo"],
     }
 
-    def test_parses_packages(self):
-        self.assertEqual(list(self.dh.packages.keys()),
-                         ['python3-foo-ext', 'foo', 'recfoo'])
+    def test_parses_packages(self) -> None:
+        self.assertEqual(
+            list(self.dh.packages.keys()), ["python3-foo-ext", "foo", "recfoo"]
+        )
+
 
 class TestControlNoBinaryPackages(DebHelperTestCase):
     control = [
-        'Source: foo-src',
-        'Build-Depends: python3-all',
-        '',
+        "Source: foo-src",
+        "Build-Depends: python3-all",
+        "",
     ]
     parse_control = False
 
-    def test_throws_error(self):
-        msg = 'Unable to parse debian/control, found less than 2 paragraphs'
+    def test_throws_error(self) -> None:
+        msg = "Unable to parse debian/control, found less than 2 paragraphs"
         with self.assertRaisesRegex(Exception, msg):
             DebHelper(self.build_options())
 
 
 class TestControlMissingPackage(DebHelperTestCase):
     control = [
-        'Source: foo-src',
-        'Build-Depends: python3-all',
-        '',
-        'Architecture: all',
+        "Source: foo-src",
+        "Build-Depends: python3-all",
+        "",
+        "Architecture: all",
     ]
     parse_control = False
 
-    def test_parses_packages(self):
-        msg = ('Unable to parse debian/control, paragraph 2 missing Package '
-               'field')
+    def test_parses_packages(self) -> None:
+        msg = "Unable to parse debian/control, paragraph 2 missing Package " "field"
         with self.assertRaisesRegex(Exception, msg):
             DebHelper(self.build_options())
 
@@ -166,18 +174,19 @@ class TestControlMissingPackage(DebHelpe
 class TestRemainingPackages(DebHelperTestCase):
     control = CONTROL
     options = {
-        'remaining_packages': True,
+        "remaining_packages": True,
     }
     parse_control = False
 
-    def setUp(self):
+    def setUp(self) -> None:
         super().setUp()
-        with open('debian/python3-foo.debhelper.log', 'w', encoding="UTF-8") as f:
-            f.write('dh_python3\n')
-        with open('debian/python3-foo-ext.debhelper.log', 'w', encoding="UTF-") as f:
-            f.write('dh_foobar\n')
+        with open("debian/python3-foo.debhelper.log", "w", encoding="UTF-8") as f:
+            f.write("dh_python3\n")
+        with open("debian/python3-foo-ext.debhelper.log", "w", encoding="UTF-") as f:
+            f.write("dh_foobar\n")
         self.dh = DebHelper(self.build_options(), impl=self.impl)
 
-    def test_skips_logged_packages(self):
-        self.assertEqual(list(self.dh.packages.keys()),
-                         ['python3-foo-ext', 'foo', 'recfoo'])
+    def test_skips_logged_packages(self) -> None:
+        self.assertEqual(
+            list(self.dh.packages.keys()), ["python3-foo-ext", "foo", "recfoo"]
+        )
diff -pruN 6.20251204.1/tests/test_depends.py 6.20251221/tests/test_depends.py
--- 6.20251204.1/tests/test_depends.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_depends.py	2025-12-21 21:07:09.000000000 +0000
@@ -2,85 +2,106 @@ import os
 import logging
 import platform
 import unittest
-from copy import deepcopy
-from pickle import dumps
+from argparse import Namespace
+from unittest.mock import patch
 from tempfile import TemporaryDirectory
+from typing import Any, Callable, Sequence, cast
 
+from dhpython.fs import ScanResult
+from dhpython.pydist import PyDist, Standard
 from dhpython.depends import Dependencies
 from dhpython.version import Version
 
 from .common import FakeOptions
 
 
-def pep386(d):
+def pep386(d: dict[str, str]) -> dict[str, list[PyDist]]:
     """Mark all pydist entries as being PEP386"""
-    for k, v in d.items():
-        if isinstance(v, str):
-            d[k] = {'dependency': v}
-            d[k].setdefault('standard', 'PEP386')
-    return d
+    result: dict[str, list[PyDist]] = {}
+    for name, dependency in d.items():
+        result[name] = [
+            pydist(name=name, dependency=dependency, standard=Standard.PEP386)
+        ]
+    return result
+
+
+def pydist(
+    name: str,
+    dependency: str,
+    versions: set[Version] = set(),
+    standard: Standard | None = None,
+    rules: list[str] | None = None,
+) -> PyDist:
+    return PyDist(
+        name=name,
+        versions=versions,
+        dependency=dependency,
+        standard=standard,
+        rules=rules or [],
+    )
 
 
-def prime_pydist(impl, pydist):
+def prime_pydist(impl: str, contents: dict[str, list[PyDist]]) -> Callable[[], None]:
     """Fake the pydist data for impl. Returns a cleanup function"""
-    from dhpython.pydist import load, normalize_name
+    from dhpython.pydist import normalize_name
 
     normalized_pydist = {}
-    for name, entries in pydist.items():
-        if not isinstance(entries, list):
-            entries = [entries]
+    for name, entries in contents.items():
         normalized_pydist[normalize_name(name)] = entries
-        for i, entry in enumerate(entries):
-            if isinstance(entry, str):
-                entries[i] = entry = {'dependency': entry}
-            entry.setdefault('name', name)
-            entry.setdefault('standard', '')
-            entry.setdefault('rules', [])
-            entry.setdefault('versions', set())
-
-    key = dumps(((impl,), {}))
-    load.cache[key] = normalized_pydist
-    return lambda: load.cache.pop(key)
+
+    def fake(req_impl: str) -> dict[str, list[PyDist]]:
+        if req_impl == impl:
+            return normalized_pydist
+        return {}
+
+    patcher = patch("dhpython.pydist.load", side_effect=fake)
+    patcher.start()
+    return patcher.stop
 
 
 class DependenciesTestCase(unittest.TestCase):
-    pkg = 'foo'
-    impl = 'cpython3'
-    pydist = {}
-    stats = {}
-    requires = {}
-    dist_info_metadata = {}
+    pkg = "foo"
+    impl = "cpython3"
+    pydist: dict[str, list[PyDist]] = {}
+    stats: dict[str, Any] = {}
+    prepared_stats: ScanResult
+    requires: dict[str, Sequence[str]] = {}
+    dist_info_metadata: dict[str, Sequence[str]] = {}
     options = FakeOptions()
     parse = True
 
-    def setUp(self):
+    def setUp(self) -> None:
         self.d = Dependencies(self.pkg, self.impl)
 
-        stats = {
-            'compile': False,
-            'dist-info': set(),
-            'egg-info': set(),
-            'ext_no_version': set(),
-            'ext_stableabi': set(),
-            'ext_vers': set(),
-            'nsp.txt': set(),
-            'private_dirs': {},
-            'public_vers': set(),
-            'requires.txt': set(),
-            'shebangs': set(),
-        }
-        stats.update(self.stats)
+        stats = ScanResult(
+            {
+                "compile": False,
+                "dist-info": set(),
+                "egg-info": set(),
+                "ext_no_version": set(),
+                "ext_stableabi": set(),
+                "ext_vers": set(),
+                "nsp.txt": set(),
+                "private_dirs": {},
+                "public_vers": set(),
+                "requires.txt": set(),
+                "shebangs": set(),
+            }
+        )
+        for k, v in self.stats.items():
+            assert k in stats
+            stats[k] = v  # type: ignore
 
         write_files = {}
         if self.requires:
             for fn, lines in self.requires.items():
                 write_files[fn] = lines
-                stats['requires.txt'].add(fn)
+                stats["requires.txt"].add(fn)
 
         if self.dist_info_metadata:
             for fn, lines in self.dist_info_metadata.items():
                 write_files[fn] = lines
-                stats['dist-info'].add(fn)
+                stats["dist-info"].add(fn)
 
         if write_files:
             self.tempdir = TemporaryDirectory()  # pylint: disable=consider-using-with
@@ -91,201 +112,209 @@ class DependenciesTestCase(unittest.Test
 
         for fn, lines in write_files.items():
             os.makedirs(os.path.dirname(fn))
-            with open(fn, 'w', encoding="UTF-8") as f:
-                f.write('\n'.join(lines))
+            with open(fn, "w", encoding="UTF-8") as f:
+                f.write("\n".join(lines))
 
         cleanup = prime_pydist(self.impl, self.pydist)
         self.addCleanup(cleanup)
 
         if self.parse:
-            self.d.parse(stats, self.options)
+            self.d.parse(stats, cast(Namespace, self.options))
         else:
             self.prepared_stats = stats
 
-    def assertNotInDepends(self, pkg):
+    def assertNotInDepends(self, pkg: str) -> None:
         """Assert that pkg doesn't appear *anywhere* in self.d.depends"""
         for dep in self.d.depends:
-            for alt in dep.split('|'):
-                alt = alt.strip().split('(', 1)[0].strip()
+            for alt in dep.split("|"):
+                alt = alt.strip().split("(", 1)[0].strip()
                 if pkg == alt:
-                    raise AssertionError(f'{pkg} appears in {alt}')
+                    raise AssertionError(f"{pkg} appears in {alt}")
 
 
 class TestRequiresCPython3(DependenciesTestCase):
     options = FakeOptions(guess_deps=True)
     pydist = {
-        'bar': 'python3-bar',
-        'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'},
-        'quux': {'dependency': 'python3-quux', 'standard': 'PEP440'},
+        "bar": [pydist(name="bar", dependency="python3-bar")],
+        "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
+        "quux": [
+            pydist(name="quux", dependency="python3-quux", standard=Standard.PEP440)
+        ],
     }
     requires = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': (
-            'bar',
-            'baz >= 1.0',
-            'quux >= 1.0a1',
+        "debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt": (
+            "bar",
+            "baz >= 1.0",
+            "quux >= 1.0a1",
         ),
     }
 
-    def test_depends_on_bar(self):
-        self.assertIn('python3-bar', self.d.depends)
+    def test_depends_on_bar(self) -> None:
+        self.assertIn("python3-bar", self.d.depends)
 
-    def test_depends_on_baz(self):
-        self.assertIn('python3-baz (>= 1.0)', self.d.depends)
+    def test_depends_on_baz(self) -> None:
+        self.assertIn("python3-baz (>= 1.0)", self.d.depends)
 
-    def test_depends_on_quux(self):
-        self.assertIn('python3-quux (>= 1.0~a1)', self.d.depends)
+    def test_depends_on_quux(self) -> None:
+        self.assertIn("python3-quux (>= 1.0~a1)", self.d.depends)
 
 
 class TestRequiresCompatible(DependenciesTestCase):
     options = FakeOptions(guess_deps=True)
     pydist = {
-        'bar': 'python3-bar',
-        'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'},
-        'qux': {'dependency': 'python3-qux', 'standard': 'PEP386'},
-        'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'},
-        'corge': {'dependency': 'python3-corge', 'standard': 'PEP440'},
+        "bar": [pydist(name="bar", dependency="python3-bar")],
+        "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
+        "qux": [pydist(name="qux", dependency="python3-qux", standard=Standard.PEP386)],
+        "quux": [
+            pydist(name="quux", dependency="python3-quux", standard=Standard.PEP386)
+        ],
+        "corge": [
+            pydist(name="corge", dependency="python3-corge", standard=Standard.PEP440)
+        ],
     }
     requires = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': (
-            'bar',
-            'baz ~= 1.1',
-            'qux == 1.*',
-            'quux',
-            'corge == 1.0',
+        "debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt": (
+            "bar",
+            "baz ~= 1.1",
+            "qux == 1.*",
+            "quux",
+            "corge == 1.0",
         ),
     }
 
-    def test_depends_on_bar(self):
-        self.assertIn('python3-bar', self.d.depends)
+    def test_depends_on_bar(self) -> None:
+        self.assertIn("python3-bar", self.d.depends)
 
-    def test_depends_on_baz(self):
-        self.assertIn('python3-baz (>= 1.1), python3-baz (<< 2)', self.d.depends)
+    def test_depends_on_baz(self) -> None:
+        self.assertIn("python3-baz (>= 1.1), python3-baz (<< 2)", self.d.depends)
 
-    def test_depends_on_qux(self):
-        self.assertIn('python3-qux (>= 1.0), python3-qux (<< 2)', self.d.depends)
+    def test_depends_on_qux(self) -> None:
+        self.assertIn("python3-qux (>= 1.0), python3-qux (<< 2)", self.d.depends)
 
-    def test_depends_on_corge(self):
-        self.assertIn('python3-corge (>= 1.0), python3-corge (<< 1.1~)',
-                      self.d.depends)
+    def test_depends_on_corge(self) -> None:
+        self.assertIn("python3-corge (>= 1.0), python3-corge (<< 1.1~)", self.d.depends)
 
 
 class TestRequiresDistPython3(DependenciesTestCase):
     options = FakeOptions(guess_deps=True)
     pydist = {
-        'bar': 'python3-bar',
-        'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'},
-        'qux': {'dependency': 'python3-qux', 'standard': 'PEP386'},
-        'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'},
-        'corge': {'dependency': 'python3-corge', 'standard': 'PEP440'},
+        "bar": [pydist(name="bar", dependency="python3-bar")],
+        "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
+        "qux": [pydist(name="qux", dependency="python3-qux", standard=Standard.PEP386)],
+        "quux": [
+            pydist(name="quux", dependency="python3-quux", standard=Standard.PEP386)
+        ],
+        "corge": [
+            pydist(name="corge", dependency="python3-corge", standard=Standard.PEP440)
+        ],
     }
     dist_info_metadata = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': (
-            'Requires-Dist: bar',
-            'Requires-Dist: baz >= 1.0',
-            'Requires-Dist: qux == 1.*',
-            'Requires-Dist: quux ~= 1.1',
-            'Requires-Dist: corge == 1.0',
+        "debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA": (
+            "Requires-Dist: bar",
+            "Requires-Dist: baz >= 1.0",
+            "Requires-Dist: qux == 1.*",
+            "Requires-Dist: quux ~= 1.1",
+            "Requires-Dist: corge == 1.0",
         ),
     }
 
-    def test_depends_on_bar(self):
-        self.assertIn('python3-bar', self.d.depends)
+    def test_depends_on_bar(self) -> None:
+        self.assertIn("python3-bar", self.d.depends)
+
+    def test_depends_on_baz(self) -> None:
+        self.assertIn("python3-baz (>= 1.0)", self.d.depends)
+
+    def test_depends_on_qux(self) -> None:
+        self.assertIn("python3-qux (>= 1.0), python3-qux (<< 2)", self.d.depends)
 
-    def test_depends_on_baz(self):
-        self.assertIn('python3-baz (>= 1.0)', self.d.depends)
+    def test_depends_on_quux(self) -> None:
+        self.assertIn("python3-quux (>= 1.1), python3-quux (<< 2)", self.d.depends)
 
-    def test_depends_on_qux(self):
-        self.assertIn('python3-qux (>= 1.0), python3-qux (<< 2)',
-                      self.d.depends)
-
-    def test_depends_on_quux(self):
-        self.assertIn('python3-quux (>= 1.1), python3-quux (<< 2)',
-                      self.d.depends)
-
-    def test_depends_on_corge(self):
-        self.assertIn('python3-corge (>= 1.0), python3-corge (<< 1.1~)',
-                      self.d.depends)
+    def test_depends_on_corge(self) -> None:
+        self.assertIn("python3-corge (>= 1.0), python3-corge (<< 1.1~)", self.d.depends)
 
 
 class TestEnvironmentMarkersDistInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=['feature'])
-    pydist = pep386({
-        'no_markers': 'python3-no-markers',
-        'os_posix': 'python3-os-posix',
-        'os_java': 'python3-os-java',
-        'sys_platform_linux': 'python3-sys-platform-linux',
-        'sys_platform_darwin': 'python3-sys-platform-darwin',
-        'platform_machine_x86_64': 'python3-platform-machine-x86-64',
-        'platform_machine_mips64': 'python3-platform-machine-mips64',
-        'platform_python_implementation_cpython':
-            'python3-platform-python-implementation-cpython',
-        'platform_python_implementation_jython':
-            'python3-platform-python-implementation-jython',
-        'platform_release_lt2': 'python3-platform-release-lt2',
-        'platform_release_ge2': 'python3-platform-release-ge2',
-        'platform_system_linux': 'python3-platform-system-linux',
-        'platform_system_windows': 'python3-platform-system-windows',
-        'platform_version_lt1': 'python3-platform-version-lt1',
-        'platform_version_ge1': 'python3-platform-version-ge1',
-        'python_version_ge3': 'python3-python-version-ge3',
-        'python_version_gt3': 'python3-python-version-gt3',
-        'python_version_lt3': 'python3-python-version-lt3',
-        'python_version_lt30': 'python3-python-version-lt30',
-        'python_version_lt38': 'python3-python-version-lt38',
-        'python_version_lt313': 'python3-python-version-lt313',
-        'python_version_le313': 'python3-python-version-le313',
-        'python_version_ge27': 'python3-python-version-ge27',
-        'python_version_ge313': 'python3-python-version-ge313',
-        'python_version_gt313': 'python3-python-version-gt313',
-        'python_version_eq313': 'python3-python-version-eq313',
-        'python_version_ne313': 'python3-python-version-ne313',
-        'python_version_aeq313': 'python3-python-version-aeq313',
-        'python_version_ceq313': 'python3-python-version-ceq313',
-        'python_version_weq313': 'python3-python-version-weq313',
-        'python_version_full_lt300': 'python3-python-version-full-lt300',
-        'python_version_full_lt3131': 'python3-python-version-full-lt3131',
-        'python_version_full_le3131': 'python3-python-version-full-le3131',
-        'python_version_full_ge3131': 'python3-python-version-full-ge3131',
-        'python_version_full_ge3131a1': 'python3-python-version-full-ge3131a1',
-        'python_version_full_ge3131b1post1':
-            'python3-python-version-full-ge3131b1post1',
-        'python_version_full_gt3131': 'python3-python-version-full-gt3131',
-        'python_version_full_eq3131': 'python3-python-version-full-eq3131',
-        'python_version_full_ne3131': 'python3-python-version-full-ne3131',
-        'python_version_full_aeq3131': 'python3-python-version-full-aeq3131',
-        'python_version_full_ceq3131': 'python3-python-version-full-ceq3131',
-        'python_version_full_weq313': 'python3-python-version-full-weq313',
-        'implementation_name_cpython': 'python3-implementation-name-cpython',
-        'implementation_name_pypy': 'python3-implementation-name-pypy',
-        'implementation_version_lt313': 'python3-implementation-version-lt313',
-        'implementation_version_ge313': 'python3-implementation-version-ge313',
-        'invalid_marker': 'python3-invalid-marker',
-        'extra_feature': 'python3-extra-feature',
-        'extra_test': 'python3-extra-test',
-        'complex_marker': 'python3-complex-marker',
-        'complex_marker_2': 'python3-complex-marker-2',
-        'no_markers_2': 'python3-no-markers-2',
-    })
+    options = FakeOptions(guess_deps=True, depends_section=["feature"])
+    pydist = pep386(
+        {
+            "no_markers": "python3-no-markers",
+            "os_posix": "python3-os-posix",
+            "os_java": "python3-os-java",
+            "sys_platform_linux": "python3-sys-platform-linux",
+            "sys_platform_darwin": "python3-sys-platform-darwin",
+            "platform_machine_x86_64": "python3-platform-machine-x86-64",
+            "platform_machine_mips64": "python3-platform-machine-mips64",
+            "platform_python_implementation_cpython": "python3-platform-python-implementation-cpython",
+            "platform_python_implementation_jython": "python3-platform-python-implementation-jython",
+            "platform_python_implementation_not_pypy": "python3-platform-python-implementation-not-pypy",
+            "platform_python_implementation_pypy": "python3-platform-python-implementation-pypy",
+            "platform_release_lt2": "python3-platform-release-lt2",
+            "platform_release_ge2": "python3-platform-release-ge2",
+            "platform_system_linux": "python3-platform-system-linux",
+            "platform_system_windows": "python3-platform-system-windows",
+            "platform_version_lt1": "python3-platform-version-lt1",
+            "platform_version_ge1": "python3-platform-version-ge1",
+            "python_version_ge3": "python3-python-version-ge3",
+            "python_version_gt3": "python3-python-version-gt3",
+            "python_version_lt3": "python3-python-version-lt3",
+            "python_version_lt30": "python3-python-version-lt30",
+            "python_version_lt38": "python3-python-version-lt38",
+            "python_version_lt313": "python3-python-version-lt313",
+            "python_version_le313": "python3-python-version-le313",
+            "python_version_ge27": "python3-python-version-ge27",
+            "python_version_ge313": "python3-python-version-ge313",
+            "python_version_gt313": "python3-python-version-gt313",
+            "python_version_eq313": "python3-python-version-eq313",
+            "python_version_ne313": "python3-python-version-ne313",
+            "python_version_aeq313": "python3-python-version-aeq313",
+            "python_version_ceq313": "python3-python-version-ceq313",
+            "python_version_weq313": "python3-python-version-weq313",
+            "python_version_full_lt300": "python3-python-version-full-lt300",
+            "python_version_full_lt3131": "python3-python-version-full-lt3131",
+            "python_version_full_le3131": "python3-python-version-full-le3131",
+            "python_version_full_ge3131": "python3-python-version-full-ge3131",
+            "python_version_full_ge3131a1": "python3-python-version-full-ge3131a1",
+            "python_version_full_ge3131b1post1": "python3-python-version-full-ge3131b1post1",
+            "python_version_full_gt3131": "python3-python-version-full-gt3131",
+            "python_version_full_eq3131": "python3-python-version-full-eq3131",
+            "python_version_full_ne3131": "python3-python-version-full-ne3131",
+            "python_version_full_aeq3131": "python3-python-version-full-aeq3131",
+            "python_version_full_ceq3131": "python3-python-version-full-ceq3131",
+            "python_version_full_weq313": "python3-python-version-full-weq313",
+            "implementation_name_cpython": "python3-implementation-name-cpython",
+            "implementation_name_pypy": "python3-implementation-name-pypy",
+            "implementation_version_lt313": "python3-implementation-version-lt313",
+            "implementation_version_ge313": "python3-implementation-version-ge313",
+            "invalid_marker": "python3-invalid-marker",
+            "extra_feature": "python3-extra-feature",
+            "extra_test": "python3-extra-test",
+            "complex_marker": "python3-complex-marker",
+            "complex_marker_2": "python3-complex-marker-2",
+            "no_markers_2": "python3-no-markers-2",
+        }
+    )
     dist_info_metadata = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': (
+        "debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA": (
             "Requires-Dist: no_markers",
             "Requires-Dist: os_posix; (os_name == 'posix')",
             'Requires-Dist: os_java; os_name == "java"',
             "Requires-Dist: sys_platform_linux ; sys_platform == 'linux'",
             "Requires-Dist: sys_platform_darwin;sys_platform == 'darwin'",
-            "Requires-Dist: platform_machine_x86_64; "
-                "platform_machine == 'x86_64'",
-            "Requires-Dist: platform_machine_mips64; "
-                "platform_machine == 'mips64'",
+            "Requires-Dist: platform_machine_x86_64; " "platform_machine == 'x86_64'",
+            "Requires-Dist: platform_machine_mips64; " "platform_machine == 'mips64'",
             "Requires-Dist: platform_python_implementation_cpython; "
-                "platform_python_implementation == 'CPython'",
+            "platform_python_implementation == 'CPython'",
             "Requires-Dist: platform_python_implementation_jython; "
-                "platform_python_implementation == 'Jython'",
+            "platform_python_implementation == 'Jython'",
+            "Requires-Dist: platform_python_implementation_pypy; "
+            "platform_python_implementation == 'PyPy'",
+            "Requires-Dist: platform_python_implementation_not_pypy; "
+            "platform_python_implementation != 'PyPy'",
             "Requires-Dist: platform_release_lt2; platform_release < '2.0'",
             "Requires-Dist: platform_release_ge2; platform_release >= '2.0'",
             "Requires-Dist: platform_system_linux; platform_system == 'Linux'",
-            "Requires-Dist: platform_system_windows; "
-                "platform_system == 'Windows'",
+            "Requires-Dist: platform_system_windows; " "platform_system == 'Windows'",
             "Requires-Dist: platform_version_lt1; platform_version < '1'",
             "Requires-Dist: platform_version_ge1; platform_version >= '1'",
             "Requires-Dist: python_version_ge3; python_version >= '3'",
@@ -304,248 +333,292 @@ class TestEnvironmentMarkersDistInfo(Dep
             "Requires-Dist: python_version_ceq313; python_version ~= '3.13'",
             "Requires-Dist: python_version_weq313; python_version == '3.13.*'",
             "Requires-Dist: python_version_full_lt300; "
-                "python_full_version < '3.0.0'",
+            "python_full_version < '3.0.0'",
             "Requires-Dist: python_version_full_lt3131; "
-                "python_full_version < '3.13.1'",
+            "python_full_version < '3.13.1'",
             "Requires-Dist: python_version_full_le3131; "
-                "python_full_version <= '3.13.1'",
+            "python_full_version <= '3.13.1'",
             "Requires-Dist: python_version_full_gt3131; "
-                "python_full_version > '3.13.1'",
+            "python_full_version > '3.13.1'",
             "Requires-Dist: python_version_full_ge3131; "
-                "python_full_version >= '3.13.1'",
+            "python_full_version >= '3.13.1'",
             "Requires-Dist: python_version_full_ge3131a1; "
-                "python_full_version >= '3.13.1a1'",
+            "python_full_version >= '3.13.1a1'",
             "Requires-Dist: python_version_full_ge3131b1post1; "
-                "python_full_version >= '3.13.1b1.post1'",
+            "python_full_version >= '3.13.1b1.post1'",
             "Requires-Dist: python_version_full_eq3131; "
-                "python_full_version == '3.13.1'",
+            "python_full_version == '3.13.1'",
             "Requires-Dist: python_version_full_ne3131; "
-                "python_full_version != '3.13.1'",
+            "python_full_version != '3.13.1'",
             "Requires-Dist: python_version_full_aeq3131; "
-                "python_full_version === '3.13.1'",
+            "python_full_version === '3.13.1'",
             "Requires-Dist: python_version_full_ceq3131; "
-                "python_full_version ~= '3.13.1'",
+            "python_full_version ~= '3.13.1'",
             "Requires-Dist: python_version_full_weq313; "
-                "python_full_version == '3.13.*'",
+            "python_full_version == '3.13.*'",
             "Requires-Dist: implementation_name_cpython; "
-                "implementation_name == 'cpython'",
-            "Requires-Dist: implementation_name_pypy; "
-                "implementation_name == 'pypy'",
+            "implementation_name == 'cpython'",
+            "Requires-Dist: implementation_name_pypy; " "implementation_name == 'pypy'",
             "Requires-Dist: implementation_version_lt313; "
-                "implementation_version < '3.13'",
+            "implementation_version < '3.13'",
             "Requires-Dist: implementation_version_ge313; "
-                "implementation_version >= '3.13'",
+            "implementation_version >= '3.13'",
             "Requires-Dist: invalid_marker; invalid_marker > '1'",
             "Requires-Dist: extra_feature; extra == 'feature'",
             "Requires-Dist: extra_test; extra == 'test'",
             "Requires-Dist: complex_marker; os_name != 'windows' "
-                "and implementation_name == 'cpython'",
-            "Requires-Dist: complex_marker_2; (python_version > \"3.4\") "
-                "and extra == 'test'",
+            "and implementation_name == 'cpython'",
+            'Requires-Dist: complex_marker_2; (python_version > "3.4") '
+            "and extra == 'test'",
             "Requires-Dist: no_markers_2",
         ),
     }
 
-    def test_depends_on_unmarked_packages(self):
-        self.assertIn('python3-no-markers', self.d.depends)
+    def test_depends_on_unmarked_packages(self) -> None:
+        self.assertIn("python3-no-markers", self.d.depends)
 
-    def test_depends_on_posix_packages(self):
-        self.assertIn('python3-os-posix', self.d.depends)
+    def test_depends_on_posix_packages(self) -> None:
+        self.assertIn("python3-os-posix", self.d.depends)
 
-    def test_skips_non_posix_packages(self):
-        self.assertNotInDepends('python3-os-java')
+    def test_skips_non_posix_packages(self) -> None:
+        self.assertNotInDepends("python3-os-java")
 
-    def test_depends_on_linux_packages(self):
-        self.assertIn('python3-sys-platform-linux', self.d.depends)
+    def test_depends_on_linux_packages(self) -> None:
+        self.assertIn("python3-sys-platform-linux", self.d.depends)
 
-    def test_skips_darwin_packages(self):
-        self.assertNotInDepends('python3-sys-platform-darwin')
+    def test_skips_darwin_packages(self) -> None:
+        self.assertNotInDepends("python3-sys-platform-darwin")
 
-    def test_depends_on_x86_64_packages_on_x86_64(self):
-        if platform.machine() == 'x86_64':
-            self.assertIn('python3-platform-machine-x86-64', self.d.depends)
+    def test_depends_on_x86_64_packages_on_x86_64(self) -> None:
+        if platform.machine() == "x86_64":
+            self.assertIn("python3-platform-machine-x86-64", self.d.depends)
         else:
-            self.assertNotInDepends('python3-platform-machine-x86-64')
+            self.assertNotInDepends("python3-platform-machine-x86-64")
 
-    def test_depends_on_mips64_packages_on_mips64(self):
-        if platform.machine() == 'mips64':
-            self.assertIn('python3-platform-machine-mips64', self.d.depends)
+    def test_depends_on_mips64_packages_on_mips64(self) -> None:
+        if platform.machine() == "mips64":
+            self.assertIn("python3-platform-machine-mips64", self.d.depends)
         else:
-            self.assertNotInDepends('python3-platform-machine-mips64')
+            self.assertNotInDepends("python3-platform-machine-mips64")
 
-    def test_depends_on_plat_cpython_packages(self):
-        self.assertIn('python3-platform-python-implementation-cpython',
-                      self.d.depends)
+    def test_depends_on_plat_cpython_packages(self) -> None:
+        self.assertIn("python3-platform-python-implementation-cpython", self.d.depends)
 
-    def test_skips_plat_jython_packages(self):
-        self.assertNotInDepends('python3-platform-python-implementation-jython')
+    def test_depends_on_plat_pypy_packages(self) -> None:
+        self.assertIn("python3-platform-python-implementation-pypy", self.d.depends)
 
-    def test_skips_release_lt_2_packages(self):
-        self.assertNotInDepends('python3-platform-release-lt2')
+    def test_depends_on_plat_not_pypy_packages(self) -> None:
+        self.assertIn("python3-platform-python-implementation-not-pypy", self.d.depends)
 
-    def test_skips_release_gt_2_packages(self):
-        self.assertNotInDepends('python3-platform-release-ge2')
+    def test_skips_plat_jython_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-python-implementation-jython")
 
-    def test_depends_on_platform_linux_packages(self):
-        self.assertIn('python3-platform-system-linux', self.d.depends)
+    def test_skips_release_lt_2_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-release-lt2")
 
-    def test_skips_platform_windows_packages(self):
-        self.assertNotInDepends('python3-platform-system-windows')
+    def test_skips_release_gt_2_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-release-ge2")
 
-    def test_skips_platfrom_version_lt_1_packages(self):
-        self.assertNotInDepends('python3-platform-version-lt1')
+    def test_depends_on_platform_linux_packages(self) -> None:
+        self.assertIn("python3-platform-system-linux", self.d.depends)
 
-    def test_skips_platform_version_ge_1_packages(self):
-        self.assertNotInDepends('python3-platform-version-ge1')
+    def test_skips_platform_windows_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-system-windows")
 
-    def test_skips_py_version_lt_3_packages(self):
-        self.assertNotInDepends('python3-python-version-lt3')
+    def test_skips_platfrom_version_lt_1_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-version-lt1")
 
-    def test_elides_py_version_ge_3(self):
-        self.assertIn('python3-python-version-ge3', self.d.depends)
+    def test_skips_platform_version_ge_1_packages(self) -> None:
+        self.assertNotInDepends("python3-platform-version-ge1")
 
-    def test_elides_py_version_gt_3(self):
-        self.assertIn('python3-python-version-gt3', self.d.depends)
+    def test_skips_py_version_lt_3_packages(self) -> None:
+        self.assertNotInDepends("python3-python-version-lt3")
 
-    def test_skips_py_version_lt_30_packages(self):
-        self.assertNotInDepends('python3-python-version-lt30')
+    def test_elides_py_version_ge_3(self) -> None:
+        self.assertIn("python3-python-version-ge3", self.d.depends)
 
-    def test_skips_py_version_lt_38_packages(self):
-        self.assertNotInDepends('python3-python-version-lt38')
+    def test_elides_py_version_gt_3(self) -> None:
+        self.assertIn("python3-python-version-gt3", self.d.depends)
 
-    def test_depends_on_py_version_lt_313_packages(self):
-        self.assertIn('python3-python-version-lt313 '
-                      '| python3-supported-min (>= 3.13)', self.d.depends)
+    def test_skips_py_version_lt_30_packages(self) -> None:
+        self.assertNotInDepends("python3-python-version-lt30")
 
-    def test_depends_on_py_version_le_313_packages(self):
-        self.assertIn('python3-python-version-le313 '
-                      '| python3-supported-min (>> 3.13)', self.d.depends)
+    def test_skips_py_version_lt_38_packages(self) -> None:
+        self.assertNotInDepends("python3-python-version-lt38")
 
-    def test_depends_on_py_version_ge_27_packages(self):
-        self.assertIn('python3-python-version-ge27',
-                      self.d.depends)
+    def test_depends_on_py_version_lt_313_packages(self) -> None:
+        self.assertIn(
+            "python3-supported-min (>= 3.13) " "| python3-python-version-lt313",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_ge_313_packages(self):
-        self.assertIn('python3-python-version-ge313 '
-                      '| python3-supported-max (<< 3.13)', self.d.depends)
+    def test_depends_on_py_version_le_313_packages(self) -> None:
+        self.assertIn(
+            "python3-supported-min (>> 3.13) " "| python3-python-version-le313",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_gt_313_packages(self):
-        self.assertIn('python3-python-version-gt313 '
-                      '| python3-supported-max (<= 3.13)', self.d.depends)
+    def test_depends_on_py_version_ge_27_packages(self) -> None:
+        self.assertIn("python3-python-version-ge27", self.d.depends)
 
-    def test_depends_on_py_version_eq_313_packages(self):
+    def test_depends_on_py_version_ge_313_packages(self) -> None:
         self.assertIn(
-            'python3-python-version-eq313 | python3-supported-max (<< 3.13) '
-            '| python3-supported-min (>= 3.14)', self.d.depends)
+            "python3-python-version-ge313 " "| python3-supported-max (<< 3.13)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_ne_313_packages(self):
+    def test_depends_on_py_version_gt_313_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-gt313 " "| python3-supported-max (<= 3.13)",
+            self.d.depends,
+        )
+
+    def test_depends_on_py_version_eq_313_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-eq313 | python3-supported-max (<< 3.13) "
+            "| python3-supported-min (>= 3.14)",
+            self.d.depends,
+        )
+
+    def test_depends_on_py_version_ne_313_packages(self) -> None:
         # Can't be represented in Debian depends
-        self.assertIn('python3-python-version-ne313', self.d.depends)
+        self.assertIn("python3-python-version-ne313", self.d.depends)
 
-    def test_depends_on_py_version_aeq_313_packages(self):
+    def test_depends_on_py_version_aeq_313_packages(self) -> None:
         self.assertIn(
-            'python3-python-version-aeq313 | python3-supported-max (<< 3.13) '
-            '| python3-supported-min (>> 3.13)', self.d.depends)
+            "python3-python-version-aeq313 | python3-supported-max (<< 3.13) "
+            "| python3-supported-min (>> 3.13)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_ceq_313_packages(self):
+    def test_depends_on_py_version_ceq_313_packages(self) -> None:
         self.assertIn(
-            'python3-python-version-ceq313 | python3-supported-max (<< 3.13) '
-            '| python3-supported-min (>= 3.14)', self.d.depends)
+            "python3-python-version-ceq313 | python3-supported-max (<< 3.13) "
+            "| python3-supported-min (>= 3.14)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_weq_313_packages(self):
+    def test_depends_on_py_version_weq_313_packages(self) -> None:
         self.assertIn(
-            'python3-python-version-weq313 | python3-supported-max (<< 3.13) '
-            '| python3-supported-min (>= 3.14)', self.d.depends)
+            "python3-python-version-weq313 | python3-supported-max (<< 3.13) "
+            "| python3-supported-min (>= 3.14)",
+            self.d.depends,
+        )
 
-    def test_skips_py_version_full_lt_300_packages(self):
-        self.assertNotInDepends('python3-python-version-full-lt300')
+    def test_skips_py_version_full_lt_300_packages(self) -> None:
+        self.assertNotInDepends("python3-python-version-full-lt300")
 
-    def test_depends_on_py_version_full_lt_3131_packages(self):
-        self.assertIn('python3-python-version-full-lt3131 '
-                      '| python3-supported-min (>= 3.13.1)', self.d.depends)
+    def test_depends_on_py_version_full_lt_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-supported-min (>= 3.13.1) " "| python3-python-version-full-lt3131",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_le_3131_packages(self):
-        self.assertIn('python3-python-version-full-le3131 '
-                      '| python3-supported-min (>> 3.13.1)', self.d.depends)
+    def test_depends_on_py_version_full_le_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-supported-min (>> 3.13.1) " "| python3-python-version-full-le3131",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_ge_3131_packages(self):
-        self.assertIn('python3-python-version-full-ge3131 '
-                      '| python3-supported-max (<< 3.13.1)', self.d.depends)
+    def test_depends_on_py_version_full_ge_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-full-ge3131 " "| python3-supported-max (<< 3.13.1)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_ge_3131a1_packages(self):
+    def test_depends_on_py_version_full_ge_3131a1_packages(self) -> None:
         # With full PEP-440 parsing this should be (<< 3.13.1~a1)
-        self.assertIn('python3-python-version-full-ge3131a1 '
-                      '| python3-supported-max (<< 3.13.0)', self.d.depends)
+        self.assertIn(
+            "python3-python-version-full-ge3131a1 "
+            "| python3-supported-max (<< 3.13.0)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_ge_3131b1post1_packages(self):
+    def test_depends_on_py_version_full_ge_3131b1post1_packages(self) -> None:
         # With full PEP-440 parsing this should be (<< 3.13.1~b1.post1)
-        self.assertIn('python3-python-version-full-ge3131a1 '
-                      '| python3-supported-max (<< 3.13.0)', self.d.depends)
+        self.assertIn(
+            "python3-python-version-full-ge3131a1 "
+            "| python3-supported-max (<< 3.13.0)",
+            self.d.depends,
+        )
+
+    def test_depends_on_py_version_full_gt_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-full-gt3131 " "| python3-supported-max (<= 3.13.1)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_gt_3131_packages(self):
-        self.assertIn('python3-python-version-full-gt3131 '
-                      '| python3-supported-max (<= 3.13.1)', self.d.depends)
-
-    def test_depends_on_py_version_full_eq_3131_packages(self):
-        self.assertIn('python3-python-version-full-eq3131 '
-                      '| python3-supported-max (<< 3.13.1) '
-                      '| python3-supported-min (>> 3.13.1)', self.d.depends)
+    def test_depends_on_py_version_full_eq_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-full-eq3131 "
+            "| python3-supported-max (<< 3.13.1) "
+            "| python3-supported-min (>> 3.13.1)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_ne_3131_packages(self):
+    def test_depends_on_py_version_full_ne_3131_packages(self) -> None:
         # Can't be represented in Debian depends
-        self.assertIn('python3-python-version-full-ne3131', self.d.depends)
+        self.assertIn("python3-python-version-full-ne3131", self.d.depends)
 
-    def test_skips_py_version_full_aeq_3131_packages(self):
+    def test_skips_py_version_full_aeq_3131_packages(self) -> None:
         # Can't be represented in Debian depends
-        self.assertNotInDepends('python3-python-version-full-aeq3131')
+        self.assertNotInDepends("python3-python-version-full-aeq3131")
 
-    def test_depends_on_py_version_full_ceq_3131_packages(self):
-        self.assertIn('python3-python-version-full-ceq3131 '
-                      '| python3-supported-max (<< 3.13.1) '
-                      '| python3-supported-min (>= 3.14)', self.d.depends)
+    def test_depends_on_py_version_full_ceq_3131_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-full-ceq3131 "
+            "| python3-supported-max (<< 3.13.1) "
+            "| python3-supported-min (>= 3.14)",
+            self.d.depends,
+        )
 
-    def test_depends_on_py_version_full_weq_313_packages(self):
-        self.assertIn('python3-python-version-full-weq313 '
-                      '| python3-supported-max (<< 3.13) '
-                      '| python3-supported-min (>= 3.14)', self.d.depends)
+    def test_depends_on_py_version_full_weq_313_packages(self) -> None:
+        self.assertIn(
+            "python3-python-version-full-weq313 "
+            "| python3-supported-max (<< 3.13) "
+            "| python3-supported-min (>= 3.14)",
+            self.d.depends,
+        )
 
-    def test_depends_on_sys_cpython_packages(self):
-        self.assertIn('python3-implementation-name-cpython', self.d.depends)
+    def test_depends_on_sys_cpython_packages(self) -> None:
+        self.assertIn("python3-implementation-name-cpython", self.d.depends)
 
-    def test_depends_on_sys_pypy_packages(self):
-        self.assertIn('python3-implementation-name-pypy', self.d.depends)
+    def test_depends_on_sys_pypy_packages(self) -> None:
+        self.assertIn("python3-implementation-name-pypy", self.d.depends)
 
-    def test_depends_on_sys_implementation_lt313_packages(self):
-        self.assertIn('python3-implementation-version-lt313 '
-                      '| python3-supported-min (>= 3.13)',
-                      self.d.depends)
+    def test_depends_on_sys_implementation_lt313_packages(self) -> None:
+        self.assertIn(
+            "python3-supported-min (>= 3.13) " "| python3-implementation-version-lt313",
+            self.d.depends,
+        )
 
-    def test_depends_on_sys_implementation_ge313_packages(self):
-        self.assertIn('python3-implementation-version-ge313 '
-                      '| python3-supported-max (<< 3.13)',
-                      self.d.depends)
+    def test_depends_on_sys_implementation_ge313_packages(self) -> None:
+        self.assertIn(
+            "python3-implementation-version-ge313 " "| python3-supported-max (<< 3.13)",
+            self.d.depends,
+        )
 
-    def test_ignores_invalid_marker(self):
-        self.assertNotInDepends('python3-invalid-marker')
+    def test_ignores_invalid_marker(self) -> None:
+        self.assertNotInDepends("python3-invalid-marker")
 
-    def test_depends_on_extra_feature_packages(self):
-        self.assertIn('python3-extra-feature', self.d.depends)
+    def test_depends_on_extra_feature_packages(self) -> None:
+        self.assertIn("python3-extra-feature", self.d.depends)
 
-    def test_skips_extra_test_packages(self):
-        self.assertNotInDepends('python3-extra-test')
+    def test_skips_extra_test_packages(self) -> None:
+        self.assertNotInDepends("python3-extra-test")
 
-    def test_ignores_complex_environment_markers(self):
-        self.assertNotInDepends('python3-complex-marker')
-        self.assertNotInDepends('python3-complex-marker-2')
+    def test_ignores_complex_environment_markers(self) -> None:
+        self.assertNotInDepends("python3-complex-marker")
+        self.assertNotInDepends("python3-complex-marker-2")
 
-    def test_depends_on_un_marked_dependency_after_extra(self):
-        self.assertIn('python3-no-markers-2', self.d.depends)
+    def test_depends_on_un_marked_dependency_after_extra(self) -> None:
+        self.assertIn("python3-no-markers-2", self.d.depends)
 
 
 class TestEnvironmentMarkersEggInfo(TestEnvironmentMarkersDistInfo):
-    dist_info_metadata = None
+    dist_info_metadata = {}
     requires = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': (
+        "debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt": (
             "no_markers",
             "[:(os_name == 'posix')]",
             "os_posix",
@@ -563,6 +636,10 @@ class TestEnvironmentMarkersEggInfo(Test
             "platform_python_implementation_cpython",
             "[:platform_python_implementation == 'Jython']",
             "platform_python_implementation_jython",
+            "[:platform_python_implementation == 'PyPy']",
+            "platform_python_implementation_pypy",
+            "[:platform_python_implementation != 'PyPy']",
+            "platform_python_implementation_not_pypy",
             "[:platform_release < '2.0']",
             "platform_release_lt2",
             "[:platform_release >= '2.0']",
@@ -650,39 +727,39 @@ class TestEnvironmentMarkersEggInfo(Test
         ),
     }
 
-    def test_depends_on_un_marked_dependency_after_extra(self):
-        raise unittest.SkipTest('Not possible in requires.txt')
+    def test_depends_on_un_marked_dependency_after_extra(self) -> None:
+        raise unittest.SkipTest("Not possible in requires.txt")
 
 
 class TestIgnoresUnusedModulesDistInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=['feature'])
+    options = FakeOptions(guess_deps=True, depends_section=["feature"])
     dist_info_metadata = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': (
+        "debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA": (
             "Requires-Dist: unusued-complex-module ; "
-                "(sys_platform == \"darwin\") and extra == 'nativelib'",
-            "Requires-Dist: unused-win-module ; (sys_platform == \"win32\")",
+            "(sys_platform == \"darwin\") and extra == 'nativelib'",
+            'Requires-Dist: unused-win-module ; (sys_platform == "win32")',
             "Requires-Dist: unused-extra-module ; extra == 'unused'",
         ),
     }
     parse = False
 
-    def test_ignores_unused_dependencies(self):
-        if not hasattr(self, 'assertLogs'):
+    def test_ignores_unused_dependencies(self) -> None:
+        if not hasattr(self, "assertLogs"):
             raise unittest.SkipTest("Requires Python >= 3.4")
-        with self.assertLogs(logger='dhpython', level=logging.INFO) as logs:
-            self.d.parse(self.prepared_stats, self.options)
+        with self.assertLogs(logger="dhpython", level=logging.INFO) as logs:
+            self.d.parse(self.prepared_stats, cast(Namespace, self.options))
         for line in logs.output:
             self.assertTrue(
-                line.startswith(
-                    'INFO:dhpython:Ignoring complex environment marker'),
-                'Expecting only complex environment marker messages, but '
-                'got: {}'.format(line))
+                line.startswith("INFO:dhpython:Ignoring complex environment marker"),
+                "Expecting only complex environment marker messages, but "
+                "got: {}".format(line),
+            )
 
 
 class TestIgnoresUnusedModulesEggInfo(DependenciesTestCase):
-    options = FakeOptions(guess_deps=True, depends_section=['feature'])
+    options = FakeOptions(guess_deps=True, depends_section=["feature"])
     requires = {
-        'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': (
+        "debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt": (
             "[nativelib:(sys_platform == 'darwin')]",
             "unusued-complex-module",
             "[:sys_platform == 'win32']",
@@ -693,64 +770,64 @@ class TestIgnoresUnusedModulesEggInfo(De
     }
     parse = False
 
-    def test_ignores_unused_dependencies(self):
-        if not hasattr(self, 'assertNoLogs'):
+    def test_ignores_unused_dependencies(self) -> None:
+        if not hasattr(self, "assertNoLogs"):
             raise unittest.SkipTest("Requires Python >= 3.10")
-        with self.assertNoLogs(logger='dhpython', level=logging.INFO):
-            self.d.parse(self.prepared_stats, self.options)
+        with self.assertNoLogs(logger="dhpython", level=logging.INFO):
+            self.d.parse(self.prepared_stats, cast(Namespace, self.options))
 
 
 class TestCExtensionPython3(DependenciesTestCase):
     stats = {
-        'public_vers': {Version('3')},
-        'ext_vers': {Version('3.14')},
+        "public_vers": {Version("3")},
+        "ext_vers": {Version("3.14")},
     }
 
-    def test_python_depends(self):
-        self.assertIn('python3 (>= 3.14~)', self.d.depends)
-        self.assertIn('python3 (<< 3.15)', self.d.depends)
+    def test_python_depends(self) -> None:
+        self.assertIn("python3 (>= 3.14~)", self.d.depends)
+        self.assertIn("python3 (<< 3.15)", self.d.depends)
 
 
 class TestCExtensionsPython3(DependenciesTestCase):
     stats = {
-        'public_vers': {Version('3')},
-        'ext_vers': {Version('3.14'), Version('3.13')},
+        "public_vers": {Version("3")},
+        "ext_vers": {Version("3.14"), Version("3.13")},
     }
 
-    def test_python_depends(self):
-        self.assertIn('python3 (>= 3.13~)', self.d.depends)
-        self.assertIn('python3 (<< 3.15)', self.d.depends)
+    def test_python_depends(self) -> None:
+        self.assertIn("python3 (>= 3.13~)", self.d.depends)
+        self.assertIn("python3 (<< 3.15)", self.d.depends)
 
 
 class TestStableABIExtensionPython3(DependenciesTestCase):
     stats = {
-        'public_vers': {Version('3')},
-        'ext_stableabi': {'foo.abi3.so'},
+        "public_vers": {Version("3")},
+        "ext_stableabi": {"foo.abi3.so"},
     }
 
-    def test_python_depends(self):
-        self.assertIn('python3', self.d.depends)
+    def test_python_depends(self) -> None:
+        self.assertIn("python3", self.d.depends)
 
 
 class TestMixedExtensionsPython3(DependenciesTestCase):
     stats = {
-        'public_vers': {Version('3')},
-        'ext_vers': {Version('3.14'), Version('3.13')},
-        'ext_stableabi': {'foo.abi3.so'},
+        "public_vers": {Version("3")},
+        "ext_vers": {Version("3.14"), Version("3.13")},
+        "ext_stableabi": {"foo.abi3.so"},
     }
 
-    def test_python_depends(self):
-        self.assertIn('python3 (>= 3.13~)', self.d.depends)
-        self.assertIn('python3 (<< 3.15)', self.d.depends)
+    def test_python_depends(self) -> None:
+        self.assertIn("python3 (>= 3.13~)", self.d.depends)
+        self.assertIn("python3 (<< 3.15)", self.d.depends)
 
 
 class TestUnversionedExtensionPython3(DependenciesTestCase):
     stats = {
-        'public_vers': {Version('3')},
-        'ext_no_version': {'foo.so'},
+        "public_vers": {Version("3")},
+        "ext_no_version": {"foo.so"},
     }
 
-    def test_python_depends(self):
+    def test_python_depends(self) -> None:
         # This test is to maintain the status-quo where this case is only
         # handled in private modules. That's *probably* OK?
-        self.assertNotInDepends('python3')
+        self.assertNotInDepends("python3")
diff -pruN 6.20251204.1/tests/test_distutils_extra.py 6.20251221/tests/test_distutils_extra.py
--- 6.20251204.1/tests/test_distutils_extra.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_distutils_extra.py	2025-12-21 21:07:09.000000000 +0000
@@ -1,38 +1,46 @@
 import os
 import unittest
+from argparse import Namespace
 from copy import deepcopy
 from tempfile import TemporaryDirectory
+from typing import cast
 
 from dhpython.depends import Dependencies
+from dhpython.fs import ScanResult
+from dhpython.pydist import Standard
 
-from .test_depends import FakeOptions, prime_pydist
+from .common import FakeOptions
+from .test_depends import prime_pydist, pydist
 
 
 class TestDistutilsExtra(unittest.TestCase):
     options = FakeOptions(guess_deps=True)
     pydist = {
-        'bar': 'python3-bar',
-        'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'},
-        'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'},
+        "bar": [pydist(name="bar", dependency="python3-bar")],
+        "baz": [pydist(name="baz", dependency="python3-baz", standard=Standard.PEP386)],
+        "quux": [
+            pydist(name="quux", dependency="python3-quux", standard=Standard.PEP386)
+        ],
     }
-    pkg = 'foo'
-    impl = 'cpython3'
-    stats = {
-        'compile': False,
-        'dist-info': set(),
-        'egg-info': set(('PKG-INFO',)),
-        'ext_no_version': set(),
-        'ext_stableabi': set(),
-        'ext_vers': set(),
-        'nsp.txt': set(),
-        'private_dirs': {},
-        'public_vers': set(),
-        'requires.txt': set(),
-        'shebangs': set(),
-    }
-    requires = {}
+    pkg = "foo"
+    impl = "cpython3"
+    stats = ScanResult(
+        {
+            "compile": False,
+            "dist-info": set(),
+            "egg-info": set(("PKG-INFO",)),
+            "ext_no_version": set(),
+            "ext_stableabi": set(),
+            "ext_vers": set(),
+            "nsp.txt": set(),
+            "private_dirs": {},
+            "public_vers": set(),
+            "requires.txt": set(),
+            "shebangs": set(),
+        }
+    )
 
-    def test_depends_on_bar(self):
+    def test_depends_on_bar(self) -> None:
         self.d = Dependencies(self.pkg, self.impl)
         stats = deepcopy(self.stats)
         self.tempdir = TemporaryDirectory()  # pylint: disable=consider-using-with
@@ -40,8 +48,9 @@ class TestDistutilsExtra(unittest.TestCa
         old_wd = os.getcwd()
         os.chdir(self.tempdir.name)
         self.addCleanup(os.chdir, old_wd)
-        with open(self.tempdir.name + '/PKG-INFO', 'w', encoding="UTF-8") as f:
-            f.write("""Metadata-Version: 2.1
+        with open(self.tempdir.name + "/PKG-INFO", "w", encoding="UTF-8") as f:
+            f.write(
+                """Metadata-Version: 2.1
 Name: gTranscribe
 Version: 0.11
 Summary: gTranscribe
@@ -52,8 +61,9 @@ License: GPL-3
 Requires: bar
 
 gTranscribe is a software focused on easy transcription of spoken words.
-""")
+"""
+            )
         cleanup = prime_pydist(self.impl, self.pydist)
         self.addCleanup(cleanup)
-        self.d.parse(stats, self.options)
-        self.assertIn('python3-bar', self.d.depends)
+        self.d.parse(stats, cast(Namespace, self.options))
+        self.assertIn("python3-bar", self.d.depends)
diff -pruN 6.20251204.1/tests/test_fs.py 6.20251221/tests/test_fs.py
--- 6.20251204.1/tests/test_fs.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_fs.py	2025-12-21 21:07:09.000000000 +0000
@@ -1,6 +1,8 @@
 import os
-from tempfile import TemporaryDirectory
+from argparse import Namespace
 from pathlib import Path
+from tempfile import TemporaryDirectory
+from typing import Any, Sequence, cast
 from unittest import TestCase
 
 from dhpython.interpreter import Interpreter
@@ -11,8 +13,9 @@ from .common import FakeOptions
 
 
 class FSTestCase(TestCase):
-    files = {}
-    def setUp(self):
+    files: dict[str, Sequence[str]] = {}
+
+    def setUp(self) -> None:
         self.tempdir = TemporaryDirectory()  # pylint: disable=consider-using-with
         self.addCleanup(self.tempdir.cleanup)
         temp_path = Path(self.tempdir.name)
@@ -20,119 +23,130 @@ class FSTestCase(TestCase):
             path = temp_path / fn
             setattr(self, path.name, path)
             path.parent.mkdir(parents=True, exist_ok=True)
-            path.write_text('\n'.join(contents) + '\n')
+            path.write_text("\n".join(contents) + "\n")
 
-    def assertFileContents(self, path, contents):
+    def assertFileContents(self, path: Path, contents: str | Sequence[str]) -> None:
         """Assert that the contents of path is contents
 
         Contents may be specified as a list of strings, one per line, without
         line-breaks.
         """
         if isinstance(contents, (list, tuple)):
-            contents = '\n'.join(contents) + '\n'
-        with path.open('r') as f:
+            contents = "\n".join(contents) + "\n"
+        assert isinstance(contents, str)
+        with path.open("r") as f:
             self.assertMultiLineEqual(contents, f.read())
 
 
 class MergeTagsTest(FSTestCase):
+    a: Path
+    b: Path
     files = {
-        'a': ('foo', 'Tag: A'),
-        'b': ('foo', 'Tag: B'),
+        "a": ("foo", "Tag: A"),
+        "b": ("foo", "Tag: B"),
     }
 
-    def test_merge_wheel(self):
+    def test_merge_wheel(self) -> None:
         merge_WHEEL(self.a, self.b)
-        self.assertFileContents(self.b, ('foo', 'Tag: B', 'Tag: A'))
+        self.assertFileContents(self.b, ("foo", "Tag: B", "Tag: A"))
 
 
 class ShareFilesTestCase(FSTestCase):
-    impl = 'cpython3'
-    options = {}
+    impl = "cpython3"
+    options: dict[str, Any] = {}
 
-    def setUp(self):
+    def setUp(self) -> None:
         super().setUp()
         self.destdir = TemporaryDirectory()  # pylint: disable=consider-using-with
         self.addCleanup(self.destdir.cleanup)
         self.interpreter = Interpreter(self.impl)
 
-    def share_files(self, options=None):
+    def share_files(self, options: dict[str, Any] | None = None) -> None:
         if options is None:
             options = self.options
-        share_files(self.tempdir.name, self.destdir.name, self.interpreter,
-                    FakeOptions(**options))
+        share_files(
+            self.tempdir.name,
+            self.destdir.name,
+            self.interpreter,
+            cast(Namespace, FakeOptions(**options)),
+        )
 
-    def destPath(self, name):
+    def destPath(self, name: str) -> Path:
         return Path(self.destdir.name) / name
 
 
 class HatchlingLicenseTest(ShareFilesTestCase):
     files = {
-        'foo.dist-info/license_files/LICENSE.txt': ('foo'),
-        'foo.dist-info/licenses/COPYING': ('foo'),
-        'foo.dist-info/RECORD': (
-            'foo.dist-info/license_files/LICENSE.txt,sha256=2c26b46b68ffc68ff99'
-            'b453c1d30413413422d706483bfa0f98a5e886266e7ae,4',
-            'foo.dist-info/licenses/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30'
-            '413413422d706483bfa0f98a5e886266e7ae,4',
-            'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea'
-            'c85d1844f5940f51c1fcba725,6'),
-        'foo.dist-info/WHEEL': ('foo'),
+        "foo.dist-info/license_files/LICENSE.txt": ("foo"),
+        "foo.dist-info/licenses/COPYING": ("foo"),
+        "foo.dist-info/RECORD": (
+            "foo.dist-info/license_files/LICENSE.txt,sha256=2c26b46b68ffc68ff99"
+            "b453c1d30413413422d706483bfa0f98a5e886266e7ae,4",
+            "foo.dist-info/licenses/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30"
+            "413413422d706483bfa0f98a5e886266e7ae,4",
+            "foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea"
+            "c85d1844f5940f51c1fcba725,6",
+        ),
+        "foo.dist-info/WHEEL": ("foo"),
     }
 
-    def test_removes_license_files(self):
+    def test_removes_license_files(self) -> None:
         self.share_files()
         self.assertFalse(
-            self.destPath('foo.dist-info/license_files/LICENSE.txt').exists())
-        self.assertFalse(
-            self.destPath('foo.dist-info/licenses/COPYING').exists())
+            self.destPath("foo.dist-info/license_files/LICENSE.txt").exists()
+        )
+        self.assertFalse(self.destPath("foo.dist-info/licenses/COPYING").exists())
 
 
 class FlitLicenseTest(ShareFilesTestCase):
     files = {
-        'foo.dist-info/COPYING': ('foo'),
-        'foo.dist-info/COPYING.LESSER': ('foo'),
-        'foo.dist-info/RECORD': (
-            'foo.dist-info/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30413413422'
-            'd706483bfa0f98a5e886266e7ae,4',
-            'foo.dist-info/COPYING.LESSER,sha256=2c26b46b68ffc68ff99b453c1d3041'
-            '3413422d706483bfa0f98a5e886266e7ae,4',
-            'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea'
-            'c85d1844f5940f51c1fcba725,6'),
-        'foo.dist-info/WHEEL': ('foo'),
+        "foo.dist-info/COPYING": ("foo"),
+        "foo.dist-info/COPYING.LESSER": ("foo"),
+        "foo.dist-info/RECORD": (
+            "foo.dist-info/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30413413422"
+            "d706483bfa0f98a5e886266e7ae,4",
+            "foo.dist-info/COPYING.LESSER,sha256=2c26b46b68ffc68ff99b453c1d3041"
+            "3413422d706483bfa0f98a5e886266e7ae,4",
+            "foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea"
+            "c85d1844f5940f51c1fcba725,6",
+        ),
+        "foo.dist-info/WHEEL": ("foo"),
     }
 
-    def test_removes_license_files(self):
+    def test_removes_license_files(self) -> None:
         self.share_files()
-        self.assertFalse(self.destPath('foo.dist-info/COPYING.LESSER').exists())
-        self.assertFalse(self.destPath('foo.dist-info/COPYING').exists())
+        self.assertFalse(self.destPath("foo.dist-info/COPYING.LESSER").exists())
+        self.assertFalse(self.destPath("foo.dist-info/COPYING").exists())
 
 
 class CExtensionsRenameTest(ShareFilesTestCase):
-    def setUp(self):
+    def setUp(self) -> None:
         ver = Interpreter(self.impl).default_version
         self.files = {
-            f'usr/lib/python{ver}/foo.so': ('binary-data'),
+            f"usr/lib/python{ver}/foo.so": ("binary-data"),
         }
-        self.ext_filename = f'usr/lib/python{ver}/foo.so'
+        self.ext_filename = f"usr/lib/python{ver}/foo.so"
         super().setUp()
 
-    def test_python_extensions(self):
+    def test_python_extensions(self) -> None:
         self.share_files()
         self.assertFalse(self.destPath(self.ext_filename).exists())
         expected_name = self.interpreter.check_extname(self.ext_filename)
+        self.assertTrue(expected_name)
+        assert expected_name
         self.assertTrue(self.destPath(expected_name).exists())
 
-    def test_no_ext_rename(self):
+    def test_no_ext_rename(self) -> None:
         self.share_files(options={"no_ext_rename": True})
         self.assertTrue(self.destPath(self.ext_filename).exists())
 
 
 class ScanTestCase(FSTestCase):
-    impl = 'cpython3'
-    options = {}
+    impl = "cpython3"
+    options: dict[str, Any] = {}
     package = "python3-test"
 
-    def setUp(self):
+    def setUp(self) -> None:
         super().setUp()
         pwd = os.getcwd()
         try:
@@ -140,7 +154,7 @@ class ScanTestCase(FSTestCase):
             self.scan = Scan(
                 interpreter=Interpreter(self.impl),
                 package=self.package,
-                options=FakeOptions(**self.options),
+                options=cast(Namespace, FakeOptions(**self.options)),
             )
         finally:
             os.chdir(pwd)
@@ -148,52 +162,52 @@ class ScanTestCase(FSTestCase):
 
 class UnversionedExtensionScanTest(ScanTestCase):
     files = {
-        'debian/python3-test/usr/lib/python3/dist-packages/foo.so': "contents",
+        "debian/python3-test/usr/lib/python3/dist-packages/foo.so": "contents",
     }
     options = {
         "no_ext_rename": True,
     }
 
-    def test_scan(self):
-        self.assertEqual(self.scan.result['public_vers'], {Version('3')})
-        self.assertEqual(self.scan.result['ext_vers'], set())
+    def test_scan(self) -> None:
+        self.assertEqual(self.scan.result["public_vers"], {Version("3")})
+        self.assertEqual(self.scan.result["ext_vers"], set())
         self.assertEqual(
-            self.scan.result['ext_no_version'],
-            {'debian/python3-test/usr/lib/python3/dist-packages/foo.so'},
+            self.scan.result["ext_no_version"],
+            {"debian/python3-test/usr/lib/python3/dist-packages/foo.so"},
         )
-        self.assertEqual(self.scan.result['ext_stableabi'], set())
+        self.assertEqual(self.scan.result["ext_stableabi"], set())
 
 
 class UnversionedExtensionRenamedScanTest(ScanTestCase):
     files = {
-        'debian/python3-test/usr/lib/python3/dist-packages/foo.so': "contents",
+        "debian/python3-test/usr/lib/python3/dist-packages/foo.so": "contents",
     }
 
-    def test_scan(self):
+    def test_scan(self) -> None:
         version = Interpreter(self.impl).default_version
-        self.assertEqual(self.scan.result['public_vers'], {Version('3')})
-        self.assertEqual(self.scan.result['ext_vers'], {version})
-        self.assertEqual(self.scan.result['ext_no_version'], set())
-        self.assertEqual(self.scan.result['ext_stableabi'], set())
+        self.assertEqual(self.scan.result["public_vers"], {Version("3")})
+        self.assertEqual(self.scan.result["ext_vers"], {version})
+        self.assertEqual(self.scan.result["ext_no_version"], set())
+        self.assertEqual(self.scan.result["ext_stableabi"], set())
 
 
 class StableABIExtensionDependenciesTest(ScanTestCase):
     files = {
         (
-            'debian/python3-test/usr/lib/python3/dist-packages/'
-            'foo.abi3-x86_64-linux-gnu.so'
+            "debian/python3-test/usr/lib/python3/dist-packages/"
+            "foo.abi3-x86_64-linux-gnu.so"
         ): "contents",
     }
 
-    def test_scan(self):
-        self.assertEqual(self.scan.result['public_vers'], {Version('3')})
-        self.assertEqual(self.scan.result['ext_vers'], set())
-        self.assertEqual(self.scan.result['ext_no_version'], set())
+    def test_scan(self) -> None:
+        self.assertEqual(self.scan.result["public_vers"], {Version("3")})
+        self.assertEqual(self.scan.result["ext_vers"], set())
+        self.assertEqual(self.scan.result["ext_no_version"], set())
         self.assertEqual(
-            self.scan.result['ext_stableabi'],
+            self.scan.result["ext_stableabi"],
             {
-                'debian/python3-test/usr/lib/python3/dist-packages/'
-                'foo.abi3-x86_64-linux-gnu.so'
+                "debian/python3-test/usr/lib/python3/dist-packages/"
+                "foo.abi3-x86_64-linux-gnu.so"
             },
         )
 
@@ -201,13 +215,13 @@ class StableABIExtensionDependenciesTest
 class VersionedExtensionDependenciesTest(ScanTestCase):
     files = {
         (
-            'debian/python3-test/usr/lib/python3/dist-packages/'
-            'foo.cpython-314-x86_64-linux-gnu.so'
+            "debian/python3-test/usr/lib/python3/dist-packages/"
+            "foo.cpython-314-x86_64-linux-gnu.so"
         ): "contents",
     }
 
-    def test_scan(self):
-        self.assertEqual(self.scan.result['public_vers'], {Version('3')})
-        self.assertEqual(self.scan.result['ext_vers'], {Version('3.14')})
-        self.assertEqual(self.scan.result['ext_no_version'], set())
-        self.assertEqual(self.scan.result['ext_stableabi'], set())
+    def test_scan(self) -> None:
+        self.assertEqual(self.scan.result["public_vers"], {Version("3")})
+        self.assertEqual(self.scan.result["ext_vers"], {Version("3.14")})
+        self.assertEqual(self.scan.result["ext_no_version"], set())
+        self.assertEqual(self.scan.result["ext_stableabi"], set())
diff -pruN 6.20251204.1/tests/test_interpreter.py 6.20251221/tests/test_interpreter.py
--- 6.20251204.1/tests/test_interpreter.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_interpreter.py	2025-12-21 21:07:09.000000000 +0000
@@ -6,134 +6,197 @@ from dhpython.interpreter import Interpr
 
 
 class TestInterpreter(unittest.TestCase):
-    def setUp(self):
-        self._triplet = environ.get('DEB_HOST_MULTIARCH')
-        environ['DEB_HOST_MULTIARCH'] = 'MYARCH'
+    def setUp(self) -> None:
+        self._triplet = environ.get("DEB_HOST_MULTIARCH")
+        environ["DEB_HOST_MULTIARCH"] = "MYARCH"
 
-    def tearDown(self):
+    def tearDown(self) -> None:
         if self._triplet:
-            environ['DEB_HOST_MULTIARCH'] = self._triplet
+            environ["DEB_HOST_MULTIARCH"] = self._triplet
         else:
-            del environ['DEB_HOST_MULTIARCH']
+            del environ["DEB_HOST_MULTIARCH"]
 
-    @unittest.skipUnless(exists('/usr/bin/python3.11'), 'python3.11 is not installed')
-    def test_python311(self):
-        i = Interpreter('python3.11')
-        self.assertEqual(i.soabi(), 'cpython-311')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-311-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-311-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.cpython-311.so'), r'foo.cpython-311-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.abi3.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-311-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.11-dbg'), 'python3.11-dbg is not installed')
-    def test_python311dbg(self):
-        i = Interpreter('python3.11-dbg')
-        self.assertEqual(i.soabi(), 'cpython-311d')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-311d-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-311-OTHER.so'))  # different architecture
-        self.assertIsNone(i.check_extname('foo.abi3.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-311d-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.12'), 'python3.12 is not installed')
-    def test_python312(self):
-        i = Interpreter('python3.12')
-        self.assertEqual(i.soabi(), 'cpython-312')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-312-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-312-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.cpython-312.so'), r'foo.cpython-312-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.abi3.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-312-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.12-dbg'), 'python3.12-dbg is not installed')
-    def test_python312dbg(self):
-        i = Interpreter('python3.12-dbg')
-        self.assertEqual(i.soabi(), 'cpython-312d')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-312d-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-312-OTHER.so'))  # different architecture
-        self.assertIsNone(i.check_extname('foo.abi3.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-312d-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.13'), 'python3.13 is not installed')
-    def test_python313(self):
-        i = Interpreter('python3.13')
-        self.assertEqual(i.soabi(), 'cpython-313')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-313-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-33m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-313-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.cpython-313.so'), r'foo.cpython-313-MYARCH.so')
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-313-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.13-dbg'), 'python3.13-dbg is not installed')
-    def test_python313dbg(self):
-        i = Interpreter('python3.13-dbg')
-        self.assertEqual(i.soabi(), 'cpython-313d')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-313d-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-33m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-313-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-313d-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.14'), 'python3.14 is not installed')
-    def test_python314(self):
-        i = Interpreter('python3.14')
-        self.assertEqual(i.soabi(), 'cpython-314')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-314-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-34m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-314-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.cpython-314.so'), r'foo.cpython-314-MYARCH.so')
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-314-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3.14-dbg'), 'python3.14-dbg is not installed')
-    def test_python314dbg(self):
-        i = Interpreter('python3.14-dbg')
-        self.assertEqual(i.soabi(), 'cpython-314d')
-        self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-314d-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-34m.so'))  # different version
-        self.assertIsNone(i.check_extname('foo.cpython-314-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-314d-MYARCH.so')
-
-    def test_python3(self):
-        i = Interpreter('python{}.{}'.format(*sys.version_info[:2]))
-        pyver = '{}{}'.format(*sys.version_info[:2])
-        self.assertEqual(i.soabi(), 'cpython-' + pyver)
-        self.assertEqual(i.check_extname('foo.so'), rf'foo.cpython-{pyver}-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname(f'foo.cpython-{pyver}-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname(f'foo.cpython-{pyver}.so'), rf'foo.cpython-{pyver}-MYARCH.so')
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), rf'foo/bar/baz.cpython-{pyver}-MYARCH.so')
-
-    @unittest.skipUnless(exists('/usr/bin/python3-dbg'), 'python3-dbg is not installed')
-    def test_python3dbg(self):
-        i = Interpreter('python{}.{}-dbg'.format(*sys.version_info[:2]))
-        pyver = '{}{}'.format(*sys.version_info[:2])
-        self.assertEqual(i.soabi(), f'cpython-{pyver}d')
-        self.assertEqual(i.check_extname('foo.so'), rf'foo.cpython-{pyver}d-MYARCH.so')
-        self.assertIsNone(i.check_extname('foo.cpython-32m.so'))  # different version
-        self.assertIsNone(i.check_extname(f'foo.cpython-{pyver}-OTHER.so'))  # different architecture
-        self.assertEqual(i.check_extname('foo.abi3.so'), "foo.abi3-MYARCH.so")
-        self.assertIsNone(i.check_extname('foo.abi3-OTHER.so'))
-        self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), rf'foo/bar/baz.cpython-{pyver}d-MYARCH.so')
-
-    def test_bare_module(self):
-        i = Interpreter('python{}.{}'.format(*sys.version_info[:2]))
-        pyver = '{}{}'.format(*sys.version_info[:2])
-        self.assertEqual(i.check_extname('module.so'), rf'module.cpython-{pyver}-MYARCH.so')
-        self.assertEqual(i.check_extname('_module.so'), rf'_module.cpython-{pyver}-MYARCH.so')
+    @unittest.skipUnless(exists("/usr/bin/python3.11"), "python3.11 is not installed")
+    def test_python311(self) -> None:
+        i = Interpreter("python3.11")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-311-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-311-OTHER.so")
+        )  # different architecture
+        self.assertEqual(
+            i.check_extname("foo.cpython-311.so"), r"foo.cpython-311-MYARCH.so"
+        )
+        self.assertIsNone(i.check_extname("foo.abi3.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-311-MYARCH.so",
+        )
+
+    @unittest.skipUnless(
+        exists("/usr/bin/python3.11-dbg"), "python3.11-dbg is not installed"
+    )
+    def test_python311dbg(self) -> None:
+        i = Interpreter("python3.11-dbg")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-311d-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-311-OTHER.so")
+        )  # different architecture
+        self.assertIsNone(i.check_extname("foo.abi3.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-311d-MYARCH.so",
+        )
+
+    @unittest.skipUnless(exists("/usr/bin/python3.12"), "python3.12 is not installed")
+    def test_python312(self) -> None:
+        i = Interpreter("python3.12")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-312-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-312-OTHER.so")
+        )  # different architecture
+        self.assertEqual(
+            i.check_extname("foo.cpython-312.so"), r"foo.cpython-312-MYARCH.so"
+        )
+        self.assertIsNone(i.check_extname("foo.abi3.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-312-MYARCH.so",
+        )
+
+    @unittest.skipUnless(
+        exists("/usr/bin/python3.12-dbg"), "python3.12-dbg is not installed"
+    )
+    def test_python312dbg(self) -> None:
+        i = Interpreter("python3.12-dbg")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-312d-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-312-OTHER.so")
+        )  # different architecture
+        self.assertIsNone(i.check_extname("foo.abi3.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-312d-MYARCH.so",
+        )
+
+    @unittest.skipUnless(exists("/usr/bin/python3.13"), "python3.13 is not installed")
+    def test_python313(self) -> None:
+        i = Interpreter("python3.13")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-313-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-33m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-313-OTHER.so")
+        )  # different architecture
+        self.assertEqual(
+            i.check_extname("foo.cpython-313.so"), r"foo.cpython-313-MYARCH.so"
+        )
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-313-MYARCH.so",
+        )
+
+    @unittest.skipUnless(
+        exists("/usr/bin/python3.13-dbg"), "python3.13-dbg is not installed"
+    )
+    def test_python313dbg(self) -> None:
+        i = Interpreter("python3.13-dbg")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-313d-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-33m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-313-OTHER.so")
+        )  # different architecture
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-313d-MYARCH.so",
+        )
+
+    @unittest.skipUnless(exists("/usr/bin/python3.14"), "python3.14 is not installed")
+    def test_python314(self) -> None:
+        i = Interpreter("python3.14")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-314-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-34m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-314-OTHER.so")
+        )  # different architecture
+        self.assertEqual(
+            i.check_extname("foo.cpython-314.so"), r"foo.cpython-314-MYARCH.so"
+        )
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-314-MYARCH.so",
+        )
+
+    @unittest.skipUnless(
+        exists("/usr/bin/python3.14-dbg"), "python3.14-dbg is not installed"
+    )
+    def test_python314dbg(self) -> None:
+        i = Interpreter("python3.14-dbg")
+        self.assertEqual(i.check_extname("foo.so"), r"foo.cpython-314d-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-34m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname("foo.cpython-314-OTHER.so")
+        )  # different architecture
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            r"foo/bar/baz.cpython-314d-MYARCH.so",
+        )
+
+    def test_python3(self) -> None:
+        i = Interpreter("python{}.{}".format(*sys.version_info[:2]))
+        pyver = "{}{}".format(*sys.version_info[:2])
+        self.assertEqual(i.check_extname("foo.so"), rf"foo.cpython-{pyver}-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname(f"foo.cpython-{pyver}-OTHER.so")
+        )  # different architecture
+        self.assertEqual(
+            i.check_extname(f"foo.cpython-{pyver}.so"),
+            rf"foo.cpython-{pyver}-MYARCH.so",
+        )
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            rf"foo/bar/baz.cpython-{pyver}-MYARCH.so",
+        )
+
+    @unittest.skipUnless(exists("/usr/bin/python3-dbg"), "python3-dbg is not installed")
+    def test_python3dbg(self) -> None:
+        i = Interpreter("python{}.{}-dbg".format(*sys.version_info[:2]))
+        pyver = "{}{}".format(*sys.version_info[:2])
+        self.assertEqual(i.check_extname("foo.so"), rf"foo.cpython-{pyver}d-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.cpython-32m.so"))  # different version
+        self.assertIsNone(
+            i.check_extname(f"foo.cpython-{pyver}-OTHER.so")
+        )  # different architecture
+        self.assertEqual(i.check_extname("foo.abi3.so"), "foo.abi3-MYARCH.so")
+        self.assertIsNone(i.check_extname("foo.abi3-OTHER.so"))
+        self.assertEqual(
+            i.check_extname("foo/bar/bazmodule.so"),
+            rf"foo/bar/baz.cpython-{pyver}d-MYARCH.so",
+        )
+
+    def test_bare_module(self) -> None:
+        i = Interpreter("python{}.{}".format(*sys.version_info[:2]))
+        pyver = "{}{}".format(*sys.version_info[:2])
+        self.assertEqual(
+            i.check_extname("module.so"), rf"module.cpython-{pyver}-MYARCH.so"
+        )
+        self.assertEqual(
+            i.check_extname("_module.so"), rf"_module.cpython-{pyver}-MYARCH.so"
+        )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff -pruN 6.20251204.1/tests/test_tools.py 6.20251221/tests/test_tools.py
--- 6.20251204.1/tests/test_tools.py	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/test_tools.py	2025-12-21 21:07:09.000000000 +0000
@@ -7,107 +7,107 @@ from dhpython.tools import fix_shebang,
 
 
 class TestRelpath(unittest.TestCase):
-    def test_common_parent_dir(self):
-        r = relpath('/usr/share/python-foo/foo.py', '/usr/bin/foo')
-        self.assertEqual(r, '../share/python-foo/foo.py')
-
-    def test_strips_common_prefix(self):
-        r = relpath('/usr/share/python-foo/foo.py', '/usr/share')
-        self.assertEqual(r, 'python-foo/foo.py')
-
-    def test_trailing_slash_ignored(self):
-        r = relpath('/usr/share/python-foo/foo.py', '/usr/share/')
-        self.assertEqual(r, 'python-foo/foo.py')
+    def test_common_parent_dir(self) -> None:
+        r = relpath("/usr/share/python-foo/foo.py", "/usr/bin/foo")
+        self.assertEqual(r, "../share/python-foo/foo.py")
+
+    def test_strips_common_prefix(self) -> None:
+        r = relpath("/usr/share/python-foo/foo.py", "/usr/share")
+        self.assertEqual(r, "python-foo/foo.py")
+
+    def test_trailing_slash_ignored(self) -> None:
+        r = relpath("/usr/share/python-foo/foo.py", "/usr/share/")
+        self.assertEqual(r, "python-foo/foo.py")
 
 
 class TestMoveMatchingFiles(unittest.TestCase):
-    def setUp(self):
+    def setUp(self) -> None:
         self.tmpdir = TemporaryDirectory()  # pylint: disable=consider-using-with
         self.addCleanup(self.tmpdir.cleanup)
-        os.makedirs(self.tmppath('foo/bar/a/b/c/spam'))
-        for path in ('foo/bar/a/b/c/spam/file.so',
-                     'foo/bar/a/b/c/spam/file.py'):
-            with open(self.tmppath(path), 'wb'):
+        os.makedirs(self.tmppath("foo/bar/a/b/c/spam"))
+        for path in ("foo/bar/a/b/c/spam/file.so", "foo/bar/a/b/c/spam/file.py"):
+            with open(self.tmppath(path), "wb"):
                 # create a 0 byte file for the test
                 pass
 
-        move_matching_files(self.tmppath('foo/bar/'),
-                            self.tmppath('foo/baz/'),
-                            r'spam/.*\.so$')
+        move_matching_files(
+            self.tmppath("foo/bar/"), self.tmppath("foo/baz/"), r"spam/.*\.so$"
+        )
 
-    def tmppath(self, *path):
+    def tmppath(self, *path: str) -> str:
         return os.path.join(self.tmpdir.name, *path)
 
-    def test_moved_matching_file(self):
-        self.assertTrue(os.path.exists(
-            self.tmppath('foo/baz/a/b/c/spam/file.so')))
-
-    def test_left_non_matching_file(self):
-        self.assertTrue(os.path.exists(
-            self.tmppath('foo/bar/a/b/c/spam/file.py')))
+    def test_moved_matching_file(self) -> None:
+        self.assertTrue(os.path.exists(self.tmppath("foo/baz/a/b/c/spam/file.so")))
+
+    def test_left_non_matching_file(self) -> None:
+        self.assertTrue(os.path.exists(self.tmppath("foo/bar/a/b/c/spam/file.py")))
 
 
 class TestFixShebang(unittest.TestCase):
-    def setUp(self):
-        self.tmpfile = Path(NamedTemporaryFile(
-            prefix="dhptest_", suffix="_shebang.py", delete=False).name)
+    def setUp(self) -> None:
+        self.tmpfile = Path(
+            NamedTemporaryFile(
+                prefix="dhptest_", suffix="_shebang.py", delete=False
+            ).name
+        )
         self.addCleanup(self.tmpfile.unlink)
 
-    def write_shebang(self, shebang):
+    def write_shebang(self, shebang: str) -> None:
         self.tmpfile.write_text(shebang + "\nprint('This is Python')\n")
 
-    def assert_shebang(self, shebang):
+    def assert_shebang(self, shebang: str) -> None:
         contents = self.tmpfile.read_text().splitlines()
         self.assertEqual(len(contents), 2)
         self.assertEqual(contents[0], shebang)
         self.assertEqual(contents[1], "print('This is Python')")
 
-    def test_perl(self):
+    def test_perl(self) -> None:
         self.write_shebang("#!/usr/bin/perl")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#!/usr/bin/perl")
 
-    def test_unversioned(self):
+    def test_unversioned(self) -> None:
         self.write_shebang("#!/usr/bin/python")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#! /usr/bin/python3")
 
-    def test_python2(self):
+    def test_python2(self) -> None:
         self.write_shebang("#!/usr/bin/python2")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#!/usr/bin/python2")
 
-    def test_python2_7(self):
+    def test_python2_7(self) -> None:
         self.write_shebang("#!/usr/bin/python2.7")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#!/usr/bin/python2.7")
 
-    def test_python3(self):
+    def test_python3(self) -> None:
         self.write_shebang("#!/usr/bin/python3")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#! /usr/bin/python3")
 
-    def test_python3_13(self):
+    def test_python3_13(self) -> None:
         self.write_shebang("#!/usr/bin/python3.13")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#!/usr/bin/python3.13")
 
-    def test_env_unversioned(self):
+    def test_env_unversioned(self) -> None:
         self.write_shebang("#!/usr/bin/env python")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#! /usr/bin/python3")
 
-    def test_env_python3(self):
+    def test_env_python3(self) -> None:
         self.write_shebang("#!/usr/bin/env python3")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#! /usr/bin/python3")
 
-    def test_env_python3_13(self):
+    def test_env_python3_13(self) -> None:
         self.write_shebang("#!/usr/bin/env python3.13")
         fix_shebang(self.tmpfile)
         self.assert_shebang("#! /usr/bin/python3.13")
 
-    def test_replacement(self):
+    def test_replacement(self) -> None:
         self.write_shebang("#!/usr/bin/env python")
         fix_shebang(self.tmpfile, "/usr/bin/foo")
         self.assert_shebang("#! /usr/bin/foo")
diff -pruN 6.20251204.1/tests/tpb02/debian/pybuild.testfiles 6.20251221/tests/tpb02/debian/pybuild.testfiles
--- 6.20251204.1/tests/tpb02/debian/pybuild.testfiles	2025-12-04 19:48:10.000000000 +0000
+++ 6.20251221/tests/tpb02/debian/pybuild.testfiles	2025-12-21 21:07:09.000000000 +0000
@@ -1,5 +1,6 @@
 testfile1.txt
 nested/testfile2.txt
 testdir
+
 testfile4.txt destination/
 testfile5.txt renamed
