diff -pruN 7.0.1-3/.zuul.yaml 7.0.3-1/.zuul.yaml
--- 7.0.1-3/.zuul.yaml	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/.zuul.yaml	2025-10-31 18:29:10.000000000 +0000
@@ -128,9 +128,12 @@
         - openstack-tox-pep8
         - build-python-release
         - openstack-tox-cover
-        - openstack-tox-py27
-        - openstack-tox-py36
-        - openstack-tox-py37
+        - openstack-tox-py27:
+            ansible-version: '9'
+        - openstack-tox-py36:
+            ansible-version: '9'
+        - openstack-tox-py37:
+            ansible-version: '9'
         - openstack-tox-py38
         - openstack-tox-py39
         - openstack-tox-py310
@@ -146,9 +149,12 @@
         - openstack-tox-pep8
         - build-python-release
         - openstack-tox-cover
-        - openstack-tox-py27
-        - openstack-tox-py36
-        - openstack-tox-py37
+        - openstack-tox-py27:
+            ansible-version: '9'
+        - openstack-tox-py36:
+            ansible-version: '9'
+        - openstack-tox-py37:
+            ansible-version: '9'
         - openstack-tox-py38
         - openstack-tox-py39
         - openstack-tox-py310
diff -pruN 7.0.1-3/debian/changelog 7.0.3-1/debian/changelog
--- 7.0.1-3/debian/changelog	2025-09-28 09:56:58.000000000 +0000
+++ 7.0.3-1/debian/changelog	2025-11-04 11:25:09.000000000 +0000
@@ -1,3 +1,10 @@
+python-pbr (7.0.3-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Add python3-packaging as build-depends.
+
+ -- Thomas Goirand <zigo@debian.org>  Tue, 04 Nov 2025 12:25:09 +0100
+
 python-pbr (7.0.1-3) unstable; urgency=medium
 
   * Uploading to unstable.
diff -pruN 7.0.1-3/debian/control 7.0.3-1/debian/control
--- 7.0.1-3/debian/control	2025-09-28 09:56:58.000000000 +0000
+++ 7.0.3-1/debian/control	2025-11-04 11:25:09.000000000 +0000
@@ -20,6 +20,7 @@ Build-Depends-Indep:
  python3-fixtures <!nocheck>,
  python3-markupsafe <!nocheck>,
  python3-openstackdocstheme <!nocheck>,
+ python3-packaging,
  python3-pip <!nocheck>,
  python3-reno <!nodoc>,
  python3-sphinxcontrib.apidoc <!nodoc>,
diff -pruN 7.0.1-3/pbr/_compat/command_hooks.py 7.0.3-1/pbr/_compat/command_hooks.py
--- 7.0.1-3/pbr/_compat/command_hooks.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/_compat/command_hooks.py	2025-10-31 18:29:10.000000000 +0000
@@ -16,11 +16,7 @@
 from __future__ import absolute_import
 from __future__ import print_function
 
-import os
-
-from setuptools.command import easy_install
-
-import pbr._compat.commands
+import pbr._compat.versions
 from pbr.hooks import base
 from pbr import options
 
@@ -44,13 +40,11 @@ class CommandsConfig(base.BaseConfig):
         self.add_command('pbr._compat.commands.LocalEggInfo')
         self.add_command('pbr._compat.commands.LocalSDist')
         self.add_command('pbr._compat.commands.LocalInstallScripts')
-        self.add_command('pbr._compat.commands.LocalDevelop')
         self.add_command('pbr._compat.commands.LocalRPMVersion')
         self.add_command('pbr._compat.commands.LocalDebVersion')
-        if os.name != 'nt':
-            easy_install.get_script_args = (
-                pbr._compat.commands.override_get_script_args
-            )
+
+        if pbr._compat.versions.setuptools_has_develop_command:
+            self.add_command('pbr._compat.commands.LocalDevelop')
 
         use_egg = options.get_boolean_option(
             self.pbr_config, 'use-egg', 'PBR_USE_EGG'
diff -pruN 7.0.1-3/pbr/_compat/commands.py 7.0.3-1/pbr/_compat/commands.py
--- 7.0.1-3/pbr/_compat/commands.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/_compat/commands.py	2025-10-31 18:29:10.000000000 +0000
@@ -22,141 +22,37 @@ import os
 import sys
 
 import setuptools
-from setuptools.command import develop
-from setuptools.command import easy_install
 from setuptools.command import egg_info
 from setuptools.command import install
 from setuptools.command import install_scripts
 from setuptools.command import sdist
 
+import pbr._compat.easy_install
+import pbr._compat.metadata
+import pbr._compat.versions
 from pbr import extra_files
 from pbr import git
 from pbr import options
 from pbr import version
 
-_wsgi_text = """#PBR Generated from %(group)r
 
-import threading
+if pbr._compat.versions.setuptools_has_develop_command:
+    from setuptools.command import develop
 
-from %(module_name)s import %(import_target)s
+    class LocalDevelop(develop.develop):
 
-if __name__ == "__main__":
-    import argparse
-    import socket
-    import sys
-    import wsgiref.simple_server as wss
-
-    parser = argparse.ArgumentParser(
-        description=%(import_target)s.__doc__,
-        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
-        usage='%%(prog)s [-h] [--port PORT] [--host IP] -- [passed options]')
-    parser.add_argument('--port', '-p', type=int, default=8000,
-                        help='TCP port to listen on')
-    parser.add_argument('--host', '-b', default='',
-                        help='IP to bind the server to')
-    parser.add_argument('args',
-                        nargs=argparse.REMAINDER,
-                        metavar='-- [passed options]',
-                        help="'--' is the separator of the arguments used "
-                        "to start the WSGI server and the arguments passed "
-                        "to the WSGI application.")
-    args = parser.parse_args()
-    if args.args:
-        if args.args[0] == '--':
-            args.args.pop(0)
-        else:
-            parser.error("unrecognized arguments: %%s" %% ' '.join(args.args))
-    sys.argv[1:] = args.args
-    server = wss.make_server(args.host, args.port, %(invoke_target)s())
-
-    print("*" * 80)
-    print("STARTING test server %(module_name)s.%(invoke_target)s")
-    url = "http://%%s:%%d/" %% (server.server_name, server.server_port)
-    print("Available at %%s" %% url)
-    print("DANGER! For testing only, do not use in production")
-    print("*" * 80)
-    sys.stdout.flush()
-
-    server.serve_forever()
-else:
-    application = None
-    app_lock = threading.Lock()
-
-    with app_lock:
-        if application is None:
-            application = %(invoke_target)s()
-
-"""
-
-_script_text = """# PBR Generated from %(group)r
-
-import sys
-
-from %(module_name)s import %(import_target)s
-
-
-if __name__ == "__main__":
-    sys.exit(%(invoke_target)s())
-"""
-
-# the following allows us to specify different templates per entry
-# point group when generating pbr scripts.
-ENTRY_POINTS_MAP = {
-    'console_scripts': _script_text,
-    'gui_scripts': _script_text,
-    'wsgi_scripts': _wsgi_text,
-}
-
-
-def generate_script(group, entry_point, header, template):
-    """Generate the script based on the template.
-
-    :param str group: The entry-point group name, e.g., "console_scripts".
-    :param str header: The first line of the script, e.g.,
-        "!#/usr/bin/env python".
-    :param str template: The script template.
-    :returns: The templated script content
-    :rtype: str
-    """
-    if not entry_point.attrs or len(entry_point.attrs) > 2:
-        raise ValueError(
-            "Script targets must be of the form "
-            "'func' or 'Class.class_method'."
-        )
+        command_name = 'develop'
 
-    script_text = template % {
-        'group': group,
-        'module_name': entry_point.module_name,
-        'import_target': entry_point.attrs[0],
-        'invoke_target': '.'.join(entry_point.attrs),
-    }
-    return header + script_text
-
-
-def override_get_script_args(
-    dist, executable=os.path.normpath(sys.executable)
-):
-    """Override entrypoints console_script."""
-    # get_script_header() is deprecated since Setuptools 12.0
-    try:
-        header = easy_install.ScriptWriter.get_header("", executable)
-    except AttributeError:
-        header = easy_install.get_script_header("", executable)
-    for group, template in ENTRY_POINTS_MAP.items():
-        for name, ep in dist.get_entry_map(group).items():
-            yield (name, generate_script(group, ep, header, template))
-
-
-class LocalDevelop(develop.develop):
-
-    command_name = 'develop'
-
-    def install_wrapper_scripts(self, dist):
-        if sys.platform == 'win32':
-            return develop.develop.install_wrapper_scripts(self, dist)
-        if not self.exclude_scripts:
-            for args in override_get_script_args(dist):
-                self.write_script(*args)
+        def install_wrapper_scripts(self, dist):
+            if sys.platform == 'win32':
+                return develop.develop.install_wrapper_scripts(self, dist)
+            if not self.exclude_scripts:
+                for (
+                    args
+                ) in pbr._compat.easy_install.ScriptWriter.get_script_args(
+                    dist
+                ):
+                    self.write_script(*args)
 
 
 class LocalInstallScripts(install_scripts.install_scripts):
@@ -164,22 +60,8 @@ class LocalInstallScripts(install_script
 
     command_name = 'install_scripts'
 
-    def _make_wsgi_scripts_only(self, dist, executable):
-        # get_script_header() is deprecated since Setuptools 12.0
-        try:
-            header = easy_install.ScriptWriter.get_header("", executable)
-        except AttributeError:
-            header = easy_install.get_script_header("", executable)
-        wsgi_script_template = ENTRY_POINTS_MAP['wsgi_scripts']
-        for name, ep in dist.get_entry_map('wsgi_scripts').items():
-            content = generate_script(
-                'wsgi_scripts', ep, header, wsgi_script_template
-            )
-            self.write_script(name, content)
-
     def run(self):
         import distutils.command.install_scripts
-        import pkg_resources
 
         self.run_command("egg_info")
         if self.distribution.scripts:
@@ -189,20 +71,37 @@ class LocalInstallScripts(install_script
             self.outfiles = []
 
         ei_cmd = self.get_finalized_command("egg_info")
-        dist = pkg_resources.Distribution(
+        dist = pbr._compat.metadata.dist(
             ei_cmd.egg_base,
-            pkg_resources.PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info),
+            ei_cmd.egg_info,
             ei_cmd.egg_name,
             ei_cmd.egg_version,
         )
         bs_cmd = self.get_finalized_command('build_scripts')
-        executable = getattr(bs_cmd, 'executable', easy_install.sys_executable)
+        executable = getattr(
+            bs_cmd, 'executable', pbr._compat.easy_install.sys_executable
+        )
         if 'bdist_wheel' in self.distribution.have_run:
             # We're building a wheel which has no way of generating mod_wsgi
             # scripts for us. Let's build them.
             # NOTE(sigmavirus24): This needs to happen here because, as the
             # comment below indicates, no_ep is True when building a wheel.
-            self._make_wsgi_scripts_only(dist, executable)
+
+            header = pbr._compat.easy_install.ScriptWriter.get_header(
+                "", executable
+            )
+
+            wsgi_script_template = pbr._compat.easy_install.ENTRY_POINTS_MAP[
+                'wsgi_scripts'
+            ]
+            wsgi_scripts = pbr._compat.metadata.get_entry_points(
+                dist, 'wsgi_scripts'
+            )
+            for name, ep in wsgi_scripts:
+                content = pbr._compat.easy_install.generate_script(
+                    'wsgi_scripts', ep, header, wsgi_script_template
+                )
+                self.write_script(name, content)
 
         if self.no_ep:
             # no_ep is True if we're installing into an .egg file or building
@@ -210,13 +109,12 @@ class LocalInstallScripts(install_script
             # entry-points listed for this package.
             return
 
-        if os.name != 'nt':
-            get_script_args = override_get_script_args
-        else:
-            get_script_args = easy_install.get_script_args
+        if os.name == 'nt':
             executable = '"%s"' % executable
 
-        for args in get_script_args(dist, executable):
+        for args in pbr._compat.easy_install.ScriptWriter.get_script_args(
+            dist, executable
+        ):
             self.write_script(*args)
 
 
diff -pruN 7.0.1-3/pbr/_compat/easy_install.py 7.0.3-1/pbr/_compat/easy_install.py
--- 7.0.1-3/pbr/_compat/easy_install.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.0.3-1/pbr/_compat/easy_install.py	2025-10-31 18:29:10.000000000 +0000
@@ -0,0 +1,478 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Most of this code is copied from setuptools ([1], [2]), licensed under the
+# MIT license
+#
+# [1] https://github.com/pypa/setuptools/blob/v67.8.0/setuptools/command/easy_install.py
+# [2] https://github.com/pypa/setuptools/blob/v67.8.0/setuptools/_distutils/spawn.py
+
+# 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.
+
+import os
+import re
+import shlex
+import subprocess
+import sys
+import textwrap
+import warnings
+
+import pbr._compat.metadata
+
+
+shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
+"""
+Pattern matching a Python interpreter indicated in first line of a script.
+"""
+
+
+def isascii(s):
+    try:
+        s.encode('ascii')
+    except UnicodeError:
+        return False
+    return True
+
+
+def find_executable(executable, path=None):
+    """Tries to find 'executable' in the directories listed in 'path'.
+
+    A string listing directories separated by 'os.pathsep'; defaults to
+    os.environ['PATH'].  Returns the complete filename or None if not found.
+    """
+    _, ext = os.path.splitext(executable)
+    if (sys.platform == 'win32') and (ext != '.exe'):
+        executable = executable + '.exe'
+
+    if os.path.isfile(executable):
+        return executable
+
+    if path is None:
+        path = os.environ.get('PATH', None)
+        if path is None:
+            try:
+                path = os.confstr("CS_PATH")
+            except (AttributeError, ValueError):
+                # os.confstr() or CS_PATH is not available
+                path = os.defpath
+        # bpo-35755: Don't use os.defpath if the PATH environment variable is
+        # set to an empty string
+
+    # PATH='' doesn't match, whereas PATH=':' looks in the current directory
+    if not path:
+        return None
+
+    paths = path.split(os.pathsep)
+    for p in paths:
+        f = os.path.join(p, executable)
+        if os.path.isfile(f):
+            # the file exists, we have a shot at spawn working
+            return f
+    return None
+
+
+class CommandSpec(list):
+    """
+    A command spec for a #! header, specified as a list of arguments akin to
+    those passed to Popen.
+    """
+
+    options = []  # type: list[str]
+    split_args = dict()  # type: dict[str, bool]
+
+    @classmethod
+    def best(cls):
+        """
+        Choose the best CommandSpec class based on environmental conditions.
+        """
+        return cls
+
+    @classmethod
+    def _sys_executable(cls):
+        _default = os.path.normpath(sys.executable)
+        return os.environ.get('__PYVENV_LAUNCHER__', _default)
+
+    @classmethod
+    def from_param(cls, param):
+        """
+        Construct a CommandSpec from a parameter to build_scripts, which may
+        be None.
+        """
+        if isinstance(param, cls):
+            return param
+        if isinstance(param, list):
+            return cls(param)
+        if param is None:
+            return cls.from_environment()
+        # otherwise, assume it's a string.
+        return cls.from_string(param)
+
+    @classmethod
+    def from_environment(cls):
+        return cls([cls._sys_executable()])
+
+    @classmethod
+    def from_string(cls, string):
+        """
+        Construct a command spec from a simple string representing a command
+        line parseable by shlex.split.
+        """
+        items = shlex.split(string, **cls.split_args)
+        return cls(items)
+
+    def install_options(self, script_text):
+        self.options = shlex.split(self._extract_options(script_text))
+        cmdline = subprocess.list2cmdline(self)
+        if not isascii(cmdline):
+            self.options[:0] = ['-x']
+
+    @staticmethod
+    def _extract_options(orig_script):
+        """
+        Extract any options from the first line of the script.
+        """
+        first = (orig_script + '\n').splitlines()[0]
+        match = shebang_pattern.match(first)
+        options = match.group(1) or '' if match else ''
+        return options.strip()
+
+    def as_header(self):
+        return self._render(self + list(self.options))
+
+    @staticmethod
+    def _strip_quotes(item):
+        _QUOTES = '"\''
+        for q in _QUOTES:
+            if item.startswith(q) and item.endswith(q):
+                return item[1:-1]
+        return item
+
+    @staticmethod
+    def _render(items):
+        cmdline = subprocess.list2cmdline(
+            CommandSpec._strip_quotes(item.strip()) for item in items
+        )
+        return '#!' + cmdline + '\n'
+
+
+sys_executable = CommandSpec._sys_executable
+
+
+class WindowsCommandSpec(CommandSpec):
+    split_args = dict(posix=False)
+
+
+_wsgi_text = """#PBR Generated from %(group)r
+
+import threading
+
+from %(module_name)s import %(import_target)s
+
+if __name__ == "__main__":
+    import argparse
+    import socket
+    import sys
+    import wsgiref.simple_server as wss
+
+    parser = argparse.ArgumentParser(
+        description=%(import_target)s.__doc__,
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        usage='%%(prog)s [-h] [--port PORT] [--host IP] -- [passed options]')
+    parser.add_argument('--port', '-p', type=int, default=8000,
+                        help='TCP port to listen on')
+    parser.add_argument('--host', '-b', default='',
+                        help='IP to bind the server to')
+    parser.add_argument('args',
+                        nargs=argparse.REMAINDER,
+                        metavar='-- [passed options]',
+                        help="'--' is the separator of the arguments used "
+                        "to start the WSGI server and the arguments passed "
+                        "to the WSGI application.")
+    args = parser.parse_args()
+    if args.args:
+        if args.args[0] == '--':
+            args.args.pop(0)
+        else:
+            parser.error("unrecognized arguments: %%s" %% ' '.join(args.args))
+    sys.argv[1:] = args.args
+    server = wss.make_server(args.host, args.port, %(invoke_target)s())
+
+    print("*" * 80)
+    print("STARTING test server %(module_name)s.%(invoke_target)s")
+    url = "http://%%s:%%d/" %% (server.server_name, server.server_port)
+    print("Available at %%s" %% url)
+    print("DANGER! For testing only, do not use in production")
+    print("*" * 80)
+    sys.stdout.flush()
+
+    server.serve_forever()
+else:
+    application = None
+    app_lock = threading.Lock()
+
+    with app_lock:
+        if application is None:
+            application = %(invoke_target)s()
+
+"""
+
+_script_text = """# PBR Generated from %(group)r
+
+import sys
+
+from %(module_name)s import %(import_target)s
+
+
+if __name__ == "__main__":
+    sys.exit(%(invoke_target)s())
+"""
+
+# the following allows us to specify different templates per entry
+# point group when generating pbr scripts.
+ENTRY_POINTS_MAP = {
+    'console_scripts': _script_text,
+    'gui_scripts': _script_text,
+    'wsgi_scripts': _wsgi_text,
+}
+
+
+def generate_script(group, entry_point, header, template):
+    """Generate the script based on the template.
+
+    :param str group: The entry-point group name, e.g., "console_scripts".
+    :param str header: The first line of the script, e.g.,
+        "!#/usr/bin/env python".
+    :param str template: The script template.
+    :returns: The templated script content
+    :rtype: str
+    """
+    if not entry_point.attrs or len(entry_point.attrs) > 2:
+        raise ValueError(
+            "Script targets must be of the form "
+            "'func' or 'Class.class_method'."
+        )
+
+    script_text = template % {
+        'group': group,
+        'module_name': entry_point.module_name,
+        'import_target': entry_point.attrs[0],
+        'invoke_target': '.'.join(entry_point.attrs),
+    }
+    return header + script_text
+
+
+class ScriptWriter:
+    """
+    Encapsulates behavior around writing entry point scripts for console and
+    gui apps.
+    """
+
+    command_spec_class = CommandSpec
+
+    @classmethod
+    def get_script_args(cls, dist, executable=None, wininst=False):
+        # NOTE(stephenfin): This was deprecated upstream. We opt not to
+        # deprecate it here.
+        writer = (WindowsScriptWriter if wininst else ScriptWriter).best()
+        header = cls.get_script_header("", executable, wininst)
+        return writer.get_args(dist, header)
+
+    @classmethod
+    def get_script_header(cls, script_text, executable=None, wininst=False):
+        # NOTE(stephenfin): This was deprecated upstream. We opt not to
+        # deprecate it here.
+        if wininst:
+            executable = "python.exe"
+        return cls.get_header(script_text, executable)
+
+    @classmethod
+    def get_args(cls, dist, header=None):
+        """
+        Yield write_script() argument tuples for a distribution's
+        console_scripts and gui_scripts entry points.
+        """
+        # NOTE(stephenfin): This is modified from upstream to add support for
+        # wsgi-scripts. The Windows version is unchanged.
+        if header is None:
+            header = cls.get_header()
+
+        for group, template in ENTRY_POINTS_MAP.items():
+            for name, ep in pbr._compat.metadata.get_entry_points(dist, group):
+                cls._ensure_safe_name(name)
+                yield (name, generate_script(group, ep, header, template))
+
+    @staticmethod
+    def _ensure_safe_name(name):
+        """
+        Prevent paths in *_scripts entry point names.
+        """
+        has_path_sep = re.search(r'[\\/]', name)
+        if has_path_sep:
+            raise ValueError("Path separators not allowed in script names")
+
+    @classmethod
+    def best(cls):
+        """
+        Select the best ScriptWriter for this environment.
+        """
+        if sys.platform == 'win32' or (os.name == 'java' and os._name == 'nt'):
+            return WindowsScriptWriter.best()
+        else:
+            return cls
+
+    @classmethod
+    def _get_script_args(cls, type_, name, header, script_text):
+        # Simply write the stub with no extension.
+        yield (name, header + script_text)
+
+    @classmethod
+    def get_header(cls, script_text="", executable=None):
+        """Create a #! line, getting options (if any) from script_text"""
+        cmd = cls.command_spec_class.best().from_param(executable)
+        cmd.install_options(script_text)
+        return cmd.as_header()
+
+
+class WindowsScriptWriter(ScriptWriter):
+    template = textwrap.dedent(
+        r"""
+        # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r
+        import re
+        import sys
+
+        # for compatibility with easy_install; see #2198
+        __requires__ = %(spec)r
+
+        try:
+            from importlib.metadata import distribution
+        except ImportError:
+            try:
+                from importlib_metadata import distribution
+            except ImportError:
+                from pkg_resources import load_entry_point
+
+
+        def importlib_load_entry_point(spec, group, name):
+            dist_name, _, _ = spec.partition('==')
+            matches = (
+                entry_point
+                for entry_point in distribution(dist_name).entry_points
+                if entry_point.group == group and entry_point.name == name
+            )
+            return next(matches).load()
+
+
+        globals().setdefault('load_entry_point', importlib_load_entry_point)
+
+
+        if __name__ == '__main__':
+            sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
+            sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)())
+        """
+    ).lstrip()
+
+    command_spec_class = WindowsCommandSpec
+
+    @classmethod
+    def get_args(cls, dist, header=None):
+        """
+        Yield write_script() argument tuples for a distribution's
+        console_scripts and gui_scripts entry points.
+        """
+        if header is None:
+            header = cls.get_header()
+        spec = str(dist.as_requirement())
+        for type_ in 'console', 'gui':
+            group = type_ + '_scripts'
+            for name, ep in pbr._compat.metadata.get_entry_points(dist, group):
+                cls._ensure_safe_name(name)
+                script_text = cls.template % {
+                    'spec': spec,
+                    'group': group,
+                    'name': name,
+                }
+                args = cls._get_script_args(type_, name, header, script_text)
+                for res in args:
+                    yield res
+
+    @classmethod
+    def best(cls):
+        """
+        Select the best ScriptWriter suitable for Windows
+        """
+        # NOTE(stephenfin): We don't support the
+        # WindowsExecutableLauncherWriter since it has a significant dependency
+        # on pkg_resources
+        return cls
+
+    @classmethod
+    def _get_script_args(cls, type_, name, header, script_text):
+        "For Windows, add a .py extension"
+        ext = dict(console='.pya', gui='.pyw')[type_]
+        if ext not in os.environ['PATHEXT'].lower().split(';'):
+            msg = (
+                "{ext} not listed in PATHEXT; scripts will not be "
+                "recognized as executables."
+            ).format(ext=ext)
+            warnings.warn(msg, UserWarning)
+        old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe']
+        old.remove(ext)
+        header = cls._adjust_header(type_, header)
+        blockers = [name + x for x in old]
+        yield name + ext, header + script_text, 't', blockers
+
+    @classmethod
+    def _adjust_header(cls, type_, orig_header):
+        """
+        Make sure 'pythonw' is used for gui and 'python' is used for
+        console (regardless of what sys.executable is).
+        """
+        pattern = 'pythonw.exe'
+        repl = 'python.exe'
+        if type_ == 'gui':
+            pattern, repl = repl, pattern
+        pattern_ob = re.compile(re.escape(pattern), re.IGNORECASE)
+        new_header = pattern_ob.sub(string=orig_header, repl=repl)
+        return new_header if cls._use_header(new_header) else orig_header
+
+    @staticmethod
+    def _use_header(new_header):
+        """
+        Should _adjust_header use the replaced header?
+
+        On non-windows systems, always use. On
+        Windows systems, only use the replaced header if it resolves
+        to an executable on the system.
+        """
+        clean_header = new_header[2:-1].strip('"')
+        return sys.platform != 'win32' or find_executable(clean_header)
+
+
+# for backward-compatibility
+get_script_args = ScriptWriter.get_script_args
+get_script_header = ScriptWriter.get_script_header
diff -pruN 7.0.1-3/pbr/_compat/metadata.py 7.0.3-1/pbr/_compat/metadata.py
--- 7.0.1-3/pbr/_compat/metadata.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/_compat/metadata.py	2025-10-31 18:29:10.000000000 +0000
@@ -15,6 +15,7 @@
 from __future__ import absolute_import
 from __future__ import print_function
 
+from collections import namedtuple
 import json
 import sys
 
@@ -24,6 +25,9 @@ METADATA_LIB_STDLIB = 'importlib.metadat
 METADATA_LIB_BACKPORT = 'importlib_metadata'
 METADATA_LIB_LEGACY = 'pkg_resources'
 
+entrypoint = namedtuple('entrypoint', ['module_name', 'attrs'])
+dist = namedtuple('dist', ['egg_base', 'egg_info', 'egg_name', 'egg_version'])
+
 
 def _get_metadata_lib():
     """Retrieve the correct metadata library to use."""
@@ -161,3 +165,94 @@ def get_version(package_name):
             return pkg_resources.get_distribution(package_name).version
         except pkg_resources.DistributionNotFound:
             raise PackageNotFound(package_name)
+
+
+def get_entry_points(dist, group):
+    metadata_lib = _get_metadata_lib()
+
+    if metadata_lib == METADATA_LIB_STDLIB:
+        import importlib.metadata
+
+        try:
+            dist = importlib.metadata.Distribution.at(dist.egg_info)
+        except importlib.metadata.PackageNotFoundError:
+            raise PackageNotFound(dist.egg_name)
+
+        # the stdlib library (!!!) changed its behavior in Python 3.10 :(
+        # https://docs.python.org/3.10/library/importlib.metadata.html#entry-points
+        if hasattr(importlib.metadata, 'EntryPoints'):
+            x = [
+                (
+                    ep.name,
+                    entrypoint(
+                        module_name=ep.module,
+                        attrs=ep.attr.split('.'),
+                    ),
+                )
+                for ep in dist.entry_points.select(group=group)
+            ]
+            return x
+        else:
+            x = [
+                (
+                    ep.name,
+                    entrypoint(
+                        module_name=ep.value.split(':')[0],
+                        attrs=ep.value.split(':')[1].split('.'),
+                    ),
+                )
+                for ep in dist.entry_points
+                if ep.group == group
+            ]
+            return x
+    elif metadata_lib == METADATA_LIB_BACKPORT:
+        import importlib_metadata
+
+        try:
+            dist = importlib_metadata.Distribution.at(dist.egg_info)
+        except importlib_metadata.PackageNotFoundError:
+            raise PackageNotFound(dist.egg_name)
+
+        # as above
+        if hasattr(importlib_metadata, 'EntryPoints'):
+            x = [
+                (
+                    ep.name,
+                    entrypoint(
+                        module_name=ep.module,
+                        attrs=ep.attr.split('.'),
+                    ),
+                )
+                for ep in dist.entry_points.select(group=group)
+            ]
+            return x
+        else:
+            x = [
+                (
+                    ep.name,
+                    entrypoint(
+                        module_name=ep.value.split(':')[0],
+                        attrs=ep.value.split(':')[1].split('.'),
+                    ),
+                )
+                for ep in dist.entry_points
+                if ep.group == group
+            ]
+            return x
+    else:  # METADATA_LIB_LEGACY
+        import pkg_resources
+
+        try:
+            dist = pkg_resources.Distribution(
+                dist.egg_base,
+                pkg_resources.PathMetadata(dist.egg_base, dist.egg_info),
+                dist.egg_name,
+                dist.egg_version,
+            )
+        except pkg_resources.DistributionNotFound:
+            raise PackageNotFound(dist.egg_name)
+
+        return [
+            (name, entrypoint(module_name=ep.module_name, attrs=ep.attrs))
+            for name, ep in dist.get_entry_map(group).items()
+        ]
diff -pruN 7.0.1-3/pbr/_compat/versions.py 7.0.3-1/pbr/_compat/versions.py
--- 7.0.1-3/pbr/_compat/versions.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.0.3-1/pbr/_compat/versions.py	2025-10-31 18:29:10.000000000 +0000
@@ -0,0 +1,19 @@
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from pbr import version
+
+setuptools_version = version.VersionInfo('setuptools').semantic_version()
+
+setuptools_has_develop_command = setuptools_version < version.SemanticVersion(
+    80, 0, 0
+)
diff -pruN 7.0.1-3/pbr/core.py 7.0.3-1/pbr/core.py
--- 7.0.1-3/pbr/core.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/core.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,155 +0,0 @@
-# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# Copyright (C) 2013 Association of Universities for Research in Astronomy
-#                    (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-#     1. Redistributions of source code must retain the above copyright
-#        notice, this list of conditions and the following disclaimer.
-#
-#     2. Redistributions in binary form must reproduce the above
-#        copyright notice, this list of conditions and the following
-#        disclaimer in the documentation and/or other materials provided
-#        with the distribution.
-#
-#     3. The name of AURA and its representatives may not be used to
-#        endorse or promote products derived from this software without
-#        specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
-# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-# DAMAGE.
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import logging
-import os
-import sys
-import warnings
-
-from distutils import errors
-
-from pbr._compat.five import integer_types
-from pbr._compat.five import string_type
-from pbr import util
-
-
-def pbr(dist, attr, value):
-    """Implements the actual pbr setup() keyword.
-
-    When used, this should be the only keyword in your setup() aside from
-    `setup_requires`.
-
-    If given as a string, the value of pbr is assumed to be the relative path
-    to the setup.cfg file to use.  Otherwise, if it evaluates to true, it
-    simply assumes that pbr should be used, and the default 'setup.cfg' is
-    used.
-
-    This works by reading the setup.cfg file, parsing out the supported
-    metadata and command options, and using them to rebuild the
-    `DistributionMetadata` object and set the newly added command options.
-
-    The reason for doing things this way is that a custom `Distribution` class
-    will not play nicely with setup_requires; however, this implementation may
-    not work well with distributions that do use a `Distribution` subclass.
-    """
-
-    # Distribution.finalize_options() is what calls this method. That means
-    # there is potential for recursion here. Recursion seems to be an issue
-    # particularly when using PEP517 build-system configs without
-    # setup_requires in setup.py. We can avoid the recursion by setting
-    # this canary so we don't repeat ourselves.
-    if hasattr(dist, '_pbr_initialized'):
-        return
-    dist._pbr_initialized = True
-
-    if not value:
-        return
-    if isinstance(value, string_type):
-        path = os.path.abspath(value)
-    else:
-        path = os.path.abspath('setup.cfg')
-    if not os.path.exists(path):
-        raise errors.DistutilsFileError(
-            'The setup.cfg file %s does not exist.' % path
-        )
-
-    # Converts the setup.cfg file to setup() arguments
-    try:
-        attrs = util.cfg_to_args(path, dist.script_args)
-    except Exception:
-        e = sys.exc_info()[1]
-        # NB: This will output to the console if no explicit logging has
-        # been setup - but thats fine, this is a fatal distutils error, so
-        # being pretty isn't the #1 goal.. being diagnosable is.
-        logging.exception('Error parsing')
-        raise errors.DistutilsSetupError(
-            'Error parsing %s: %s: %s' % (path, e.__class__.__name__, e)
-        )
-
-    # There are some metadata fields that are only supported by
-    # setuptools and not distutils, and hence are not in
-    # dist.metadata.  We are OK to write these in.  For gory details
-    # see
-    #  https://github.com/pypa/setuptools/pull/1343
-    _DISTUTILS_UNSUPPORTED_METADATA = (
-        'long_description_content_type',
-        'project_urls',
-        'provides_extras',
-    )
-
-    # Repeat some of the Distribution initialization code with the newly
-    # provided attrs
-    if attrs:
-        # Skips 'options' and 'licence' support which are rarely used; may
-        # add back in later if demanded
-        for key, val in attrs.items():
-            if hasattr(dist.metadata, 'set_' + key):
-                getattr(dist.metadata, 'set_' + key)(val)
-            elif hasattr(dist.metadata, key):
-                setattr(dist.metadata, key, val)
-            elif hasattr(dist, key):
-                setattr(dist, key, val)
-            elif key in _DISTUTILS_UNSUPPORTED_METADATA:
-                setattr(dist.metadata, key, val)
-            else:
-                msg = 'Unknown distribution option: %s' % repr(key)
-                warnings.warn(msg)
-
-    # Re-finalize the underlying Distribution
-    try:
-        super(dist.__class__, dist).finalize_options()
-    except TypeError:
-        # If dist is not declared as a new-style class (with object as
-        # a subclass) then super() will not work on it. This is the case
-        # for Python 2. In that case, fall back to doing this the ugly way
-        dist.__class__.__bases__[-1].finalize_options(dist)
-
-    # This bit comes out of distribute/setuptools
-    if isinstance(dist.metadata.version, integer_types + (float,)):
-        # Some people apparently take "version number" too literally :)
-        dist.metadata.version = str(dist.metadata.version)
diff -pruN 7.0.1-3/pbr/hooks/metadata.py 7.0.3-1/pbr/hooks/metadata.py
--- 7.0.1-3/pbr/hooks/metadata.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/hooks/metadata.py	2025-10-31 18:29:10.000000000 +0000
@@ -28,6 +28,9 @@ class MetadataConfig(base.BaseConfig):
         self.config['version'] = packaging.get_version(
             self.config['name'], self.config.get('version', None)
         )
+        # NOTE(stephenfin): While we are appending this to '[metadata]
+        # requires_dist' here, we immediately transform that to
+        # 'install_requires' when parsing 'setup.cfg'
         packaging.append_text_list(
             self.config, 'requires_dist', packaging.parse_requirements()
         )
diff -pruN 7.0.1-3/pbr/setupcfg.py 7.0.3-1/pbr/setupcfg.py
--- 7.0.1-3/pbr/setupcfg.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.0.3-1/pbr/setupcfg.py	2025-10-31 18:29:10.000000000 +0000
@@ -0,0 +1,847 @@
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Copyright (C) 2013 Association of Universities for Research in Astronomy
+#                    (AURA)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#     1. Redistributions of source code must retain the above copyright
+#        notice, this list of conditions and the following disclaimer.
+#
+#     2. Redistributions in binary form must reproduce the above
+#        copyright notice, this list of conditions and the following
+#        disclaimer in the documentation and/or other materials provided
+#        with the distribution.
+#
+#     3. The name of AURA and its representatives may not be used to
+#        endorse or promote products derived from this software without
+#        specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
+# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+# The code in this module is mostly copy/pasted out of the distutils2 source
+# code, as recommended by Tarek Ziade.
+
+from __future__ import absolute_import
+from __future__ import print_function
+
+# These first two imports are not used, but are needed to get around an
+# irritating Python bug that can crop up when using ./setup.py test.
+# See: http://www.eby-sarna.com/pipermail/peak/2010-May/003355.html
+try:
+    import multiprocessing  # noqa
+except ImportError:
+    pass
+import logging  # noqa
+
+import io
+import os
+import re
+import shlex
+import sys
+import traceback
+import warnings
+
+from distutils import errors
+from distutils import log
+import setuptools
+from setuptools import dist as st_dist
+from setuptools import extension
+
+from pbr._compat.five import ConfigParser
+from pbr._compat.five import integer_types
+from pbr._compat.five import string_type
+from pbr._compat import packaging as packaging_compat
+from pbr import extra_files
+from pbr import hooks
+
+"""Implementation of setup.cfg support."""
+
+# A simplified RE for this; just checks that the line ends with version
+# predicates in ()
+_VERSION_SPEC_RE = re.compile(r'\s*(.*?)\s*\((.*)\)\s*$')
+
+# Mappings from setup.cfg options, in (section, option) form, to setup()
+# keyword arguments
+CFG_TO_PY_SETUP_ARGS = (
+    (('metadata', 'name'), 'name'),
+    (('metadata', 'version'), 'version'),
+    (('metadata', 'author'), 'author'),
+    (('metadata', 'author_email'), 'author_email'),
+    (('metadata', 'maintainer'), 'maintainer'),
+    (('metadata', 'maintainer_email'), 'maintainer_email'),
+    (('metadata', 'home_page'), 'url'),
+    (('metadata', 'project_urls'), 'project_urls'),
+    (('metadata', 'summary'), 'description'),
+    (('metadata', 'keywords'), 'keywords'),
+    (('metadata', 'description'), 'long_description'),
+    (
+        ('metadata', 'description_content_type'),
+        'long_description_content_type',
+    ),
+    (('metadata', 'download_url'), 'download_url'),
+    (('metadata', 'classifier'), 'classifiers'),
+    (('metadata', 'platform'), 'platforms'),  # **
+    (('metadata', 'license'), 'license'),
+    # Use setuptools install_requires, not
+    # broken distutils requires
+    (('metadata', 'requires_dist'), 'install_requires'),
+    (('metadata', 'setup_requires_dist'), 'setup_requires'),
+    (('metadata', 'python_requires'), 'python_requires'),
+    (('metadata', 'requires_python'), 'python_requires'),
+    (('metadata', 'provides_dist'), 'provides'),  # **
+    (('metadata', 'provides_extras'), 'provides_extras'),
+    (('metadata', 'obsoletes_dist'), 'obsoletes'),  # **
+    (('files', 'packages_root'), 'package_dir'),
+    (('files', 'packages'), 'packages'),
+    (('files', 'package_data'), 'package_data'),
+    (('files', 'namespace_packages'), 'namespace_packages'),
+    (('files', 'data_files'), 'data_files'),
+    (('files', 'scripts'), 'scripts'),
+    (('files', 'modules'), 'py_modules'),  # **
+    (('global', 'commands'), 'cmdclass'),
+    # Not supported in distutils2, but provided for
+    # backwards compatibility with setuptools
+    (('backwards_compat', 'zip_safe'), 'zip_safe'),
+    (('backwards_compat', 'tests_require'), 'tests_require'),
+    (('backwards_compat', 'dependency_links'), 'dependency_links'),
+    (('backwards_compat', 'include_package_data'), 'include_package_data'),
+)
+
+DEPRECATED_CFG = {
+    ('metadata', 'home_page'): (
+        "Use '[metadata] url' (setup.cfg) or '[project.urls]' "
+        "(pyproject.toml) instead"
+    ),
+    ('metadata', 'summary'): (
+        "Use '[metadata] description' (setup.cfg) or '[project] description' "
+        "(pyproject.toml) instead"
+    ),
+    ('metadata', 'description_file'): (
+        "Use '[metadata] long_description' (setup.cfg) or '[project] readme' "
+        "(pyproject.toml) instead"
+    ),
+    ('metadata', 'classifier'): (
+        "Use '[metadata] classifiers' (setup.cfg) or '[project] classifiers' "
+        "(pyproject.toml) instead"
+    ),
+    ('metadata', 'platform'): (
+        "Use '[metadata] platforms' (setup.cfg) or "
+        "'[tool.setuptools] platforms' (pyproject.toml) instead"
+    ),
+    ('metadata', 'requires_dist'): (
+        "Use '[options] install_requires' (setup.cfg) or "
+        "'[project] dependencies' (pyproject.toml) instead"
+    ),
+    ('metadata', 'setup_requires_dist'): (
+        "Use '[options] setup_requires' (setup.cfg) or "
+        "'[build-system] requires' (pyproject.toml) instead"
+    ),
+    ('metadata', 'python_requires'): (
+        "Use '[options] python_requires' (setup.cfg) or "
+        "'[project] requires-python' (pyproject.toml) instead"
+    ),
+    ('metadata', 'requires_python'): (
+        "Use '[options] python_requires' (setup.cfg) or "
+        "'[project] requires-python' (pyproject.toml) instead"
+    ),
+    ('metadata', 'provides_dist'): "This option is ignored by pip",
+    ('metadata', 'provides_extras'): "This option is ignored by pip",
+    ('metadata', 'obsoletes_dist'): "This option is ignored by pip",
+    ('files', 'packages_root'): (
+        "Use '[options] package_dir' (setup.cfg) or '[tools.setuptools] "
+        "package_dir' (pyproject.toml) instead"
+    ),
+    ('files', 'packages'): (
+        "Use '[options] packages' (setup.cfg) or '[tools.setuptools] "
+        "packages' (pyproject.toml) instead"
+    ),
+    ('files', 'package_data'): (
+        "Use '[options.package_data]' (setup.cfg) or "
+        "'[tool.setuptools.package-data]' (pyproject.toml) instead"
+    ),
+    ('files', 'namespace_packages'): (
+        "Use '[options] namespace_packages' (setup.cfg) or migrate to PEP "
+        "420-style namespace packages instead"
+    ),
+    ('files', 'data_files'): (
+        "For package data files, use '[options] package_data' (setup.cfg) "
+        "or '[tools.setuptools] package_data' (pyproject.toml) instead. "
+        "Support for non-package data files is deprecated in setuptools "
+        "and their use is discouraged. If necessary, use "
+        "'[options] data_files' (setup.cfg) or '[tools.setuptools] data-files'"
+        "(pyproject.toml) instead."
+    ),
+    ('files', 'scripts'): (
+        "Migrate to using the console_scripts entrypoint and use "
+        "'[options.entry_points]' (setup.cfg) or '[project.scripts]' "
+        "(pyproject.toml) instead"
+    ),
+    ('files', 'modules'): (
+        "Use '[options] py_modules' (setup.cfg) or '[tools.setuptools] "
+        "py-modules' (pyproject.toml) instead"
+    ),
+    ('backwards_compat', 'zip_safe'): (
+        "This option is obsolete as it was only relevant in the context of "
+        "eggs"
+    ),
+    ('backwards_compat', 'dependency_links'): (
+        "This option is ignored by pip starting from pip 19.0"
+    ),
+    ('backwards_compat', 'tests_require'): (
+        "This option is ignored by pip starting from pip 19.0"
+    ),
+    ('backwards_compat', 'include_package_data'): (
+        "Use '[options] include_package_data' (setup.cfg) or "
+        "'[tools.setuptools] include-package-data' (pyproject.toml) instead"
+    ),
+}
+
+# setup() arguments that can have multiple values in setup.cfg
+MULTI_FIELDS = (
+    "classifiers",
+    "platforms",
+    "install_requires",
+    "provides",
+    "obsoletes",
+    "namespace_packages",
+    "packages",
+    "package_data",
+    "data_files",
+    "scripts",
+    "py_modules",
+    "dependency_links",
+    "setup_requires",
+    "tests_require",
+    "keywords",
+    "cmdclass",
+    "provides_extras",
+)
+
+# a mapping of removed keywords to the version of setuptools that they were deprecated in
+REMOVED_KEYWORDS = {
+    # https://setuptools.pypa.io/en/stable/history.html#v72-0-0
+    'tests_requires': '72.0.0',
+}
+
+# setup() arguments that can have mapping values in setup.cfg
+MAP_FIELDS = ("project_urls",)
+
+# setup() arguments that contain boolean values
+BOOL_FIELDS = ("zip_safe", "include_package_data")
+
+
+def shlex_split(path):
+    if os.name == 'nt':
+        # shlex cannot handle paths that contain backslashes, treating those
+        # as escape characters.
+        path = path.replace("\\", "/")
+        return [x.replace("/", "\\") for x in shlex.split(path)]
+
+    return shlex.split(path)
+
+
+def resolve_name(name):
+    """Resolve a name like ``module.object`` to an object and return it.
+
+    Raise ImportError if the module or name is not found.
+    """
+    parts = name.split('.')
+    cursor = len(parts) - 1
+    module_name = parts[:cursor]
+    attr_name = parts[-1]
+
+    while cursor > 0:
+        try:
+            ret = __import__('.'.join(module_name), fromlist=[attr_name])
+            break
+        except ImportError:
+            if cursor == 0:
+                raise
+            cursor -= 1
+            module_name = parts[:cursor]
+            attr_name = parts[cursor]
+            ret = ''
+
+    for part in parts[cursor:]:
+        try:
+            ret = getattr(ret, part)
+        except AttributeError:
+            raise ImportError(name)
+
+    return ret
+
+
+def setup_cfg_to_args(path='setup.cfg', script_args=None):
+    """Parse setup.cfg file.
+
+    Parse a setup.cfg file and tranform pbr-specific options to the underlying
+    setuptools opts.
+
+    :param path: The setup.cfg path.
+    :param script_args: List of commands setup.py was called with.
+    :returns: A dictionary of kwargs to set on the underlying Distribution
+        object.
+    :raises DistutilsFileError: When the setup.cfg file is not found.
+    """
+    if script_args is None:
+        script_args = ()
+
+    # The method source code really starts here.
+    parser = ConfigParser()
+
+    if not os.path.exists(path):
+        raise errors.DistutilsFileError(
+            "file '%s' does not exist" % os.path.abspath(path)
+        )
+
+    try:
+        parser.read(path, encoding='utf-8')
+    except TypeError:
+        # Python 2 doesn't accept the encoding kwarg
+        parser.read(path)
+
+    config = {}
+    for section in parser.sections():
+        config[section] = {}
+        for k, value in parser.items(section):
+            config[section][k.replace('-', '_')] = value
+
+    # Run setup_hooks, if configured
+    setup_hooks = has_get_option(config, 'global', 'setup_hooks')
+    package_dir = has_get_option(config, 'files', 'packages_root')
+
+    # Add the source package directory to sys.path in case it contains
+    # additional hooks, and to make sure it's on the path before any existing
+    # installations of the package
+    if package_dir:
+        package_dir = os.path.abspath(package_dir)
+        sys.path.insert(0, package_dir)
+
+    try:
+        if setup_hooks:
+            setup_hooks = [
+                hook
+                for hook in split_multiline(setup_hooks)
+                if hook != 'pbr.hooks.setup_hook'
+            ]
+            for hook in setup_hooks:
+                hook_fn = resolve_name(hook)
+                try:
+                    hook_fn(config)
+                except SystemExit:
+                    log.error('setup hook %s terminated the installation')
+                except Exception:
+                    e = sys.exc_info()[1]
+                    log.error(
+                        'setup hook %s raised exception: %s\n' % (hook, e)
+                    )
+                    log.error(traceback.format_exc())
+                    sys.exit(1)
+
+        # Run the pbr hook
+        hooks.setup_hook(config)
+
+        kwargs = setup_cfg_to_setup_kwargs(config, script_args)
+
+        # Set default config overrides
+        kwargs['include_package_data'] = True
+        kwargs['zip_safe'] = False
+
+        if has_get_option(config, 'global', 'compilers'):
+            warnings.warn(
+                'Support for custom compilers was removed in pbr 7.0 and the '
+                '\'[global] compilers\' option is now ignored.',
+                DeprecationWarning,
+            )
+
+        ext_modules = get_extension_modules(config)
+        if ext_modules:
+            kwargs['ext_modules'] = ext_modules
+
+        entry_points = get_entry_points(config)
+        if entry_points:
+            kwargs['entry_points'] = entry_points
+
+        # Handle the [files]/extra_files option
+        files_extra_files = has_get_option(config, 'files', 'extra_files')
+        if files_extra_files:
+            extra_files.set_extra_files(split_multiline(files_extra_files))
+
+    finally:
+        # Perform cleanup if any paths were added to sys.path
+        if package_dir:
+            sys.path.pop(0)
+
+    return kwargs
+
+
+def _read_description_file(config):
+    """Handle the legacy 'description_file' option."""
+    long_description = has_get_option(config, 'metadata', 'long_description')
+    if long_description:
+        # if we have a long_description then do nothing: setuptools will take
+        # care of this for us
+        return None
+
+    description_files = has_get_option(config, 'metadata', 'description_file')
+    if not description_files:
+        return None
+
+    description_files = split_multiline(description_files)
+
+    data = ''
+    for filename in description_files:
+        description_file = io.open(filename, encoding='utf-8')
+        try:
+            data += description_file.read().strip() + '\n\n'
+        finally:
+            description_file.close()
+
+    return data
+
+
+def setup_cfg_to_setup_kwargs(config, script_args=None):
+    """Convert config options to kwargs.
+
+    Processes the setup.cfg options and converts them to arguments accepted
+    by setuptools' setup() function.
+    """
+    if script_args is None:
+        script_args = ()
+
+    kwargs = {}
+
+    # Temporarily holds install_requires and extra_requires while we
+    # parse env_markers.
+    all_requirements = {}
+
+    # We want people to use description and long_description over summary and
+    # description but there is obvious overlap. If we see the both of the
+    # former being used, don't normalize
+    skip_description_normalization = False
+    if has_get_option(config, 'metadata', 'description') and (
+        has_get_option(config, 'metadata', 'long_description')
+        or has_get_option(config, 'metadata', 'description_file')
+    ):
+        kwargs['description'] = has_get_option(
+            config, 'metadata', 'description'
+        )
+        long_description = _read_description_file(config)
+        if long_description:
+            kwargs['long_description'] = long_description
+
+        skip_description_normalization = True
+
+    for alias, arg in CFG_TO_PY_SETUP_ARGS:
+        section, option = alias
+
+        if skip_description_normalization and alias in (
+            ('metadata', 'summary'),
+            ('metadata', 'description'),
+        ):
+            continue
+
+        in_cfg_value = has_get_option(config, section, option)
+
+        if alias == ('metadata', 'description') and not in_cfg_value:
+            in_cfg_value = _read_description_file(config)
+
+        if not in_cfg_value:
+            continue
+
+        if alias in DEPRECATED_CFG:
+            warnings.warn(
+                "The '[%s] %s' option is deprecated: %s"
+                % (alias[0], alias[1], DEPRECATED_CFG[alias]),
+                DeprecationWarning,
+            )
+
+        if arg in MULTI_FIELDS:
+            in_cfg_value = split_multiline(in_cfg_value)
+        elif arg in MAP_FIELDS:
+            in_cfg_map = {}
+            for i in split_multiline(in_cfg_value):
+                k, v = i.split('=', 1)
+                in_cfg_map[k.strip()] = v.strip()
+            in_cfg_value = in_cfg_map
+        elif arg in BOOL_FIELDS:
+            # Provide some flexibility here...
+            if in_cfg_value.lower() in ('true', 't', '1', 'yes', 'y'):
+                in_cfg_value = True
+            else:
+                in_cfg_value = False
+
+        if in_cfg_value:
+            if arg in REMOVED_KEYWORDS and (
+                packaging_compat.parse_version(setuptools.__version__)
+                >= packaging_compat.parse_version(REMOVED_KEYWORDS[arg])
+            ):
+                # deprecation warnings, if any, will already have been logged,
+                # so simply skip this
+                continue
+
+            if arg in ('install_requires', 'tests_require'):
+                # Replaces PEP345-style version specs with the sort expected by
+                # setuptools
+                in_cfg_value = [
+                    _VERSION_SPEC_RE.sub(r'\1\2', pred)
+                    for pred in in_cfg_value
+                ]
+
+            if arg == 'install_requires':
+                # Split install_requires into package,env_marker tuples
+                # These will be re-assembled later
+                install_requires = []
+                requirement_pattern = (
+                    r'(?P<package>[^;]*);?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
+                )
+                for requirement in in_cfg_value:
+                    m = re.match(requirement_pattern, requirement)
+                    requirement_package = m.group('package').strip()
+                    env_marker = m.group('env_marker').strip()
+                    install_requires.append((requirement_package, env_marker))
+                all_requirements[''] = install_requires
+            elif arg == 'package_dir':
+                in_cfg_value = {'': in_cfg_value}
+            elif arg in ('package_data', 'data_files'):
+                data_files = {}
+                firstline = True
+                prev = None
+                for line in in_cfg_value:
+                    if '=' in line:
+                        key, value = line.split('=', 1)
+                        key_unquoted = shlex_split(key.strip())[0]
+                        key, value = (key_unquoted, value.strip())
+                        if key in data_files:
+                            # Multiple duplicates of the same package name;
+                            # this is for backwards compatibility of the old
+                            # format prior to d2to1 0.2.6.
+                            prev = data_files[key]
+                            prev.extend(shlex_split(value))
+                        else:
+                            prev = data_files[key.strip()] = shlex_split(value)
+                    elif firstline:
+                        raise errors.DistutilsOptionError(
+                            'malformed package_data first line %r (misses '
+                            '"=")' % line
+                        )
+                    else:
+                        prev.extend(shlex_split(line.strip()))
+                    firstline = False
+                if arg == 'data_files':
+                    # the data_files value is a pointlessly different structure
+                    # from the package_data value
+                    data_files = sorted(data_files.items())
+                in_cfg_value = data_files
+            elif arg == 'cmdclass':
+                cmdclass = {}
+                dist = st_dist.Distribution()
+                for cls_name in in_cfg_value:
+                    cls = resolve_name(cls_name)
+                    cmd = cls(dist)
+                    cmdclass[cmd.get_command_name()] = cls
+                in_cfg_value = cmdclass
+
+        kwargs[arg] = in_cfg_value
+
+    # Transform requirements with embedded environment markers to
+    # setuptools' supported marker-per-requirement format.
+    #
+    # install_requires are treated as a special case of extras, before
+    # being put back in the expected place
+    #
+    # fred =
+    #     foo:marker
+    #     bar
+    # -> {'fred': ['bar'], 'fred:marker':['foo']}
+
+    if 'extras' in config:
+        requirement_pattern = (
+            r'(?P<package>[^:]*):?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
+        )
+        extras = config['extras']
+        # Add contents of test-requirements, if any, into an extra named
+        # 'test' if one does not already exist.
+        if 'test' not in extras:
+            from pbr import packaging
+
+            extras['test'] = "\n".join(
+                packaging.parse_requirements(packaging.TEST_REQUIREMENTS_FILES)
+            ).replace(';', ':')
+
+        for extra in extras:
+            extra_requirements = []
+            requirements = split_multiline(extras[extra])
+            for requirement in requirements:
+                m = re.match(requirement_pattern, requirement)
+                extras_value = m.group('package').strip()
+                env_marker = m.group('env_marker')
+                extra_requirements.append((extras_value, env_marker))
+            all_requirements[extra] = extra_requirements
+
+    # Transform the full list of requirements into:
+    # - install_requires, for those that have no extra and no
+    #   env_marker
+    # - named extras, for those with an extra name (which may include
+    #   an env_marker)
+    # - and as a special case, install_requires with an env_marker are
+    #   treated as named extras where the name is the empty string
+
+    extras_require = {}
+    for req_group in all_requirements:
+        for requirement, env_marker in all_requirements[req_group]:
+            if env_marker:
+                extras_key = '%s:(%s)' % (req_group, env_marker)
+                # We do not want to poison wheel creation with locally
+                # evaluated markers.  sdists always re-create the egg_info
+                # and as such do not need guarded, and pip will never call
+                # multiple setup.py commands at once.
+                if 'bdist_wheel' not in script_args:
+                    try:
+                        if packaging_compat.evaluate_marker(
+                            '(%s)' % env_marker
+                        ):
+                            extras_key = req_group
+                    except SyntaxError:
+                        log.error(
+                            "Marker evaluation failed, see the following "
+                            "error.  For more information see: "
+                            "http://docs.openstack.org/"
+                            "pbr/latest/user/using.html#environment-markers"
+                        )
+                        raise
+            else:
+                extras_key = req_group
+            extras_require.setdefault(extras_key, []).append(requirement)
+
+    kwargs['install_requires'] = extras_require.pop('', [])
+    kwargs['extras_require'] = extras_require
+
+    return kwargs
+
+
+def get_extension_modules(config):
+    """Handle extension modules"""
+
+    EXTENSION_FIELDS = (
+        "sources",
+        "include_dirs",
+        "define_macros",
+        "undef_macros",
+        "library_dirs",
+        "libraries",
+        "runtime_library_dirs",
+        "extra_objects",
+        "extra_compile_args",
+        "extra_link_args",
+        "export_symbols",
+        "swig_opts",
+        "depends",
+    )
+
+    ext_modules = []
+    for section in config:
+        if ':' in section:
+            labels = section.split(':', 1)
+        else:
+            # Backwards compatibility for old syntax; don't use this though
+            labels = section.split('=', 1)
+        labels = [label.strip() for label in labels]
+        if (len(labels) == 2) and (labels[0] == 'extension'):
+            ext_args = {}
+            for field in EXTENSION_FIELDS:
+                value = has_get_option(config, section, field)
+                # All extension module options besides name can have multiple
+                # values
+                if not value:
+                    continue
+                value = split_multiline(value)
+                if field == 'define_macros':
+                    macros = []
+                    for macro in value:
+                        macro = macro.split('=', 1)
+                        if len(macro) == 1:
+                            macro = (macro[0].strip(), None)
+                        else:
+                            macro = (macro[0].strip(), macro[1].strip())
+                        macros.append(macro)
+                    value = macros
+                ext_args[field] = value
+            if ext_args:
+                if 'name' not in ext_args:
+                    ext_args['name'] = labels[1]
+                ext_modules.append(
+                    extension.Extension(ext_args.pop('name'), **ext_args)
+                )
+    return ext_modules
+
+
+def get_entry_points(config):
+    """Process the [entry_points] section of setup.cfg."""
+
+    if 'entry_points' not in config:
+        return {}
+
+    warnings.warn(
+        "The 'entry_points' section has been deprecated in favour of the "
+        "'[options.entry_points]' section (if using 'setup.cfg') or the "
+        "'[project.scripts]' and/or '[project.entry-points.{name}]' sections "
+        "(if using 'pyproject.toml')",
+        DeprecationWarning,
+    )
+
+    return {
+        option: split_multiline(value)
+        for option, value in config['entry_points'].items()
+    }
+
+
+def has_get_option(config, section, option):
+    if section in config and option in config[section]:
+        return config[section][option]
+    else:
+        return False
+
+
+def split_multiline(value):
+    """Special behaviour when we have a multi line options"""
+    value = [
+        element
+        for element in (line.strip() for line in value.split('\n'))
+        if element and not element.startswith('#')
+    ]
+    return value
+
+
+def split_csv(value):
+    """Special behaviour when we have a comma separated options"""
+    value = [
+        element
+        for element in (chunk.strip() for chunk in value.split(','))
+        if element
+    ]
+    return value
+
+
+def pbr(dist, attr, value):
+    """Implements the pbr setup() keyword.
+
+    When used, this should be the only keyword in your setup() aside from
+    `setup_requires`.
+
+    If given as a string, the value of pbr is assumed to be the relative path
+    to the setup.cfg file to use.  Otherwise, if it evaluates to true, it
+    simply assumes that pbr should be used, and the default 'setup.cfg' is
+    used.
+
+    This works by reading the setup.cfg file, parsing out the supported
+    metadata and command options, and using them to rebuild the
+    `DistributionMetadata` object and set the newly added command options.
+
+    The reason for doing things this way is that a custom `Distribution` class
+    will not play nicely with setup_requires; however, this implementation may
+    not work well with distributions that do use a `Distribution` subclass.
+    """
+
+    # Distribution.finalize_options() is what calls this method. That means
+    # there is potential for recursion here. Recursion seems to be an issue
+    # particularly when using PEP517 build-system configs without
+    # setup_requires in setup.py. We can avoid the recursion by setting
+    # this canary so we don't repeat ourselves.
+    if hasattr(dist, '_pbr_initialized'):
+        return
+    dist._pbr_initialized = True
+
+    if not value:
+        return
+
+    if isinstance(value, string_type):
+        path = os.path.abspath(value)
+    else:
+        path = os.path.abspath('setup.cfg')
+
+    if not os.path.exists(path):
+        raise errors.DistutilsFileError(
+            'The setup.cfg file %s does not exist.' % path
+        )
+
+    # Converts the setup.cfg file to setup() arguments
+    try:
+        attrs = setup_cfg_to_args(path, dist.script_args)
+    except Exception:
+        e = sys.exc_info()[1]
+        # NB: This will output to the console if no explicit logging has
+        # been setup - but thats fine, this is a fatal distutils error, so
+        # being pretty isn't the #1 goal.. being diagnosable is.
+        logging.exception('Error parsing')
+        raise errors.DistutilsSetupError(
+            'Error parsing %s: %s: %s' % (path, e.__class__.__name__, e)
+        )
+
+    # There are some metadata fields that are only supported by
+    # setuptools and not distutils, and hence are not in
+    # dist.metadata.  We are OK to write these in.  For gory details
+    # see
+    #  https://github.com/pypa/setuptools/pull/1343
+    _DISTUTILS_UNSUPPORTED_METADATA = (
+        'long_description_content_type',
+        'project_urls',
+        'provides_extras',
+    )
+
+    # Repeat some of the Distribution initialization code with the newly
+    # provided attrs
+    if attrs:
+        # Skips 'options' and 'licence' support which are rarely used; may
+        # add back in later if demanded
+        for key, val in attrs.items():
+            if hasattr(dist.metadata, 'set_' + key):
+                getattr(dist.metadata, 'set_' + key)(val)
+            elif hasattr(dist.metadata, key):
+                setattr(dist.metadata, key, val)
+            elif hasattr(dist, key):
+                setattr(dist, key, val)
+            elif key in _DISTUTILS_UNSUPPORTED_METADATA:
+                setattr(dist.metadata, key, val)
+            else:
+                msg = 'Unknown distribution option: %s' % repr(key)
+                warnings.warn(msg)
+
+    # Re-finalize the underlying Distribution
+    try:
+        super(dist.__class__, dist).finalize_options()
+    except TypeError:
+        # If dist is not declared as a new-style class (with object as
+        # a subclass) then super() will not work on it. This is the case
+        # for Python 2. In that case, fall back to doing this the ugly way
+        dist.__class__.__bases__[-1].finalize_options(dist)
+
+    # This bit comes out of distribute/setuptools
+    if isinstance(dist.metadata.version, integer_types + (float,)):
+        # Some people apparently take "version number" too literally :)
+        dist.metadata.version = str(dist.metadata.version)
diff -pruN 7.0.1-3/pbr/tests/_compat/test_commands.py 7.0.3-1/pbr/tests/_compat/test_commands.py
--- 7.0.1-3/pbr/tests/_compat/test_commands.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/_compat/test_commands.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,101 +0,0 @@
-# Copyright (c) 2013 New Dream Network, LLC (DreamHost)
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# Copyright (C) 2013 Association of Universities for Research in Astronomy
-#                    (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-#     1. Redistributions of source code must retain the above copyright
-#        notice, this list of conditions and the following disclaimer.
-#
-#     2. Redistributions in binary form must reproduce the above
-#        copyright notice, this list of conditions and the following
-#        disclaimer in the documentation and/or other materials provided
-#        with the distribution.
-#
-#     3. The name of AURA and its representatives may not be used to
-#        endorse or promote products derived from this software without
-#        specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
-# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-
-import pkg_resources
-import testtools
-
-from pbr._compat import commands
-
-
-class TestPackagingHelpers(testtools.TestCase):
-
-    def test_generate_script(self):
-        group = 'console_scripts'
-        entry_point = pkg_resources.EntryPoint(
-            name='test-ep',
-            module_name='pbr.packaging',
-            attrs=('LocalInstallScripts',),
-        )
-        header = '#!/usr/bin/env fake-header\n'
-        template = (
-            '%(group)s %(module_name)s %(import_target)s %(invoke_target)s'
-        )
-
-        generated_script = commands.generate_script(
-            group, entry_point, header, template
-        )
-
-        expected_script = (
-            '#!/usr/bin/env fake-header\nconsole_scripts pbr.packaging '
-            'LocalInstallScripts LocalInstallScripts'
-        )
-        self.assertEqual(expected_script, generated_script)
-
-    def test_generate_script_validates_expectations(self):
-        group = 'console_scripts'
-        entry_point = pkg_resources.EntryPoint(
-            name='test-ep', module_name='pbr.packaging'
-        )
-        header = '#!/usr/bin/env fake-header\n'
-        template = (
-            '%(group)s %(module_name)s %(import_target)s %(invoke_target)s'
-        )
-        self.assertRaises(
-            ValueError,
-            commands.generate_script,
-            group,
-            entry_point,
-            header,
-            template,
-        )
-
-        entry_point = pkg_resources.EntryPoint(
-            name='test-ep',
-            module_name='pbr.packaging',
-            attrs=('attr1', 'attr2', 'attr3'),
-        )
-        self.assertRaises(
-            ValueError,
-            commands.generate_script,
-            group,
-            entry_point,
-            header,
-            template,
-        )
diff -pruN 7.0.1-3/pbr/tests/_compat/test_easy_install.py 7.0.3-1/pbr/tests/_compat/test_easy_install.py
--- 7.0.1-3/pbr/tests/_compat/test_easy_install.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.0.3-1/pbr/tests/_compat/test_easy_install.py	2025-10-31 18:29:10.000000000 +0000
@@ -0,0 +1,100 @@
+# Copyright (c) 2013 New Dream Network, LLC (DreamHost)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Copyright (C) 2013 Association of Universities for Research in Astronomy
+#                    (AURA)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#     1. Redistributions of source code must retain the above copyright
+#        notice, this list of conditions and the following disclaimer.
+#
+#     2. Redistributions in binary form must reproduce the above
+#        copyright notice, this list of conditions and the following
+#        disclaimer in the documentation and/or other materials provided
+#        with the distribution.
+#
+#     3. The name of AURA and its representatives may not be used to
+#        endorse or promote products derived from this software without
+#        specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+
+import testtools
+
+from pbr._compat import easy_install
+from pbr._compat import metadata
+
+
+class TestPackagingHelpers(testtools.TestCase):
+
+    def test_generate_script(self):
+        group = 'console_scripts'
+        entry_point = metadata.entrypoint(
+            module_name='pbr.packaging',
+            attrs=('LocalInstallScripts',),
+        )
+        header = '#!/usr/bin/env fake-header\n'
+        template = (
+            '%(group)s %(module_name)s %(import_target)s %(invoke_target)s'
+        )
+
+        generated_script = easy_install.generate_script(
+            group, entry_point, header, template
+        )
+
+        expected_script = (
+            '#!/usr/bin/env fake-header\nconsole_scripts pbr.packaging '
+            'LocalInstallScripts LocalInstallScripts'
+        )
+        self.assertEqual(expected_script, generated_script)
+
+    def test_generate_script_validates_expectations(self):
+        group = 'console_scripts'
+        entry_point = metadata.entrypoint(
+            module_name='pbr.packaging',
+            attrs=None,
+        )
+        header = '#!/usr/bin/env fake-header\n'
+        template = (
+            '%(group)s %(module_name)s %(import_target)s %(invoke_target)s'
+        )
+        self.assertRaises(
+            ValueError,
+            easy_install.generate_script,
+            group,
+            entry_point,
+            header,
+            template,
+        )
+
+        entry_point = metadata.entrypoint(
+            module_name='pbr.packaging',
+            attrs=('attr1', 'attr2', 'attr3'),
+        )
+        self.assertRaises(
+            ValueError,
+            easy_install.generate_script,
+            group,
+            entry_point,
+            header,
+            template,
+        )
diff -pruN 7.0.1-3/pbr/tests/fixtures.py 7.0.3-1/pbr/tests/fixtures.py
--- 7.0.1-3/pbr/tests/fixtures.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/fixtures.py	2025-10-31 18:29:10.000000000 +0000
@@ -278,13 +278,13 @@ class Packages(fixtures.Fixture):
                 setup_requires=['pbr'],
                 pbr=True,
             )
-        """
+            """
         ),
         'setup.cfg': textwrap.dedent(
             u"""\
             [metadata]
             name = {pkg_name}
-        """
+            """
         ),
     }
 
@@ -292,9 +292,10 @@ class Packages(fixtures.Fixture):
         """Creates packages from dict with defaults
 
         :param packages: a dict where the keys are the package name and a
-        value that is a second dict that may be empty, containing keys of
-        filenames and a string value of the contents.
-        {'package-a': {'requirements.txt': 'string', 'setup.cfg': 'string'}
+            value that is a second dict that may be empty, containing keys of
+            filenames and a string value of the contents. ::
+
+                {'package-a': {'requirements.txt': 'string', 'setup.cfg': 'string'}
         """
         self.packages = packages
 
diff -pruN 7.0.1-3/pbr/tests/functional/test_commands.py 7.0.3-1/pbr/tests/functional/test_commands.py
--- 7.0.1-3/pbr/tests/functional/test_commands.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/functional/test_commands.py	2025-10-31 18:29:10.000000000 +0000
@@ -55,7 +55,7 @@ class TestCommands(base.BaseTestCase):
         """
         self.run_setup('egg_info')
         stdout, _, _ = self.run_setup('--keywords')
-        assert stdout == 'packaging, distutils, setuptools'
+        self.assertEqual('packaging, distutils, setuptools', stdout)
 
     def test_custom_build_py_command(self):
         """Test custom build_py command.
diff -pruN 7.0.1-3/pbr/tests/functional/test_integration.py 7.0.3-1/pbr/tests/functional/test_integration.py
--- 7.0.1-3/pbr/tests/functional/test_integration.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/functional/test_integration.py	2025-10-31 18:29:10.000000000 +0000
@@ -15,11 +15,11 @@ from __future__ import absolute_import
 from __future__ import print_function
 
 import os.path
-import pkg_resources
 import shlex
 import sys
 
 import fixtures
+import packaging.utils
 import testtools
 import textwrap
 
@@ -99,7 +99,9 @@ class TestIntegration(base.BaseTestCase)
         # overheads of setup would start to beat the benefits of parallelism.
         path = os.path.join(REPODIR, self.short_name)
         setup_cfg = os.path.join(path, 'setup.cfg')
-        project_name = pkg_resources.safe_name(self.short_name).lower()
+        project_name = packaging.utils.canonicalize_name(
+            self.short_name
+        ).lower()
         # These projects should all have setup.cfg files but we'll be careful
         if os.path.exists(setup_cfg):
             config = ConfigParser()
@@ -111,7 +113,9 @@ class TestIntegration(base.BaseTestCase)
                 # Technically we should really only need to use the raw
                 # name because all our projects should be good and use
                 # normalized names but they don't...
-                project_name = pkg_resources.safe_name(raw_name).lower()
+                project_name = packaging.utils.canonicalize_name(
+                    raw_name
+                ).lower()
         constraints = os.path.join(
             REPODIR, 'requirements', 'upper-constraints.txt'
         )
@@ -124,7 +128,9 @@ class TestIntegration(base.BaseTestCase)
             with open(tmp_constraints, 'w') as dest:
                 for line in src:
                     constraint = line.split('===')[0]
-                    constraint = pkg_resources.safe_name(constraint).lower()
+                    constraint = packaging.utils.canonicalize_name(
+                        constraint
+                    ).lower()
                     if project_name != constraint:
                         dest.write(line)
         pip_cmd = PIP_CMD + ['-c', tmp_constraints]
@@ -223,6 +229,8 @@ class TestInstallWithoutPbr(base.BaseTes
         os.mkdir(test_pkg_dir)
         pkgs = {
             'pkgTest': {
+                # override setup.py, setup.cfg since we don't want pbr used for
+                # this package
                 'setup.py': textwrap.dedent(
                     """\
                     #!/usr/bin/env python
@@ -233,29 +241,31 @@ class TestInstallWithoutPbr(base.BaseTes
                         # avoid collisions?
                         install_requires = ['pkgReq'],
                     )
-                """
+                    """
                 ),
                 'setup.cfg': textwrap.dedent(
                     """\
                     [easy_install]
                     find_links = %s
-                """
+                    """
                     % dist_dir
                 ),
             },
             # We don't need to use PBRVERSION here because we precreate the
             # pbr sdist and point to it with find_links.
             'pkgReq': {
+                # ...but we use the standard setup.py, setup.cfg provided by
+                # the fixture since we do want pbr here, in the dependency
                 'requirements.txt': textwrap.dedent(
                     """\
                     pbr
-                """
+                    """
                 ),
                 'pkgReq/__init__.py': "",
                 'pkgReq/__main__.py': textwrap.dedent(
                     """\
                     print("FakeTest loaded and ran")
-                """
+                    """
                 ),
             },
         }
@@ -375,6 +385,9 @@ class TestMarkersPip(base.BaseTestCase):
                 'pip',
                 'install',
                 '--no-index',
+                # With --no-index above we can't install deps for isolated
+                # builds. Just rely on the existing installs for now.
+                '--no-build-isolation',
                 '-f',
                 repo_dir,
                 'test_markers',
diff -pruN 7.0.1-3/pbr/tests/functional/test_requirements.py 7.0.3-1/pbr/tests/functional/test_requirements.py
--- 7.0.1-3/pbr/tests/functional/test_requirements.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/functional/test_requirements.py	2025-10-31 18:29:10.000000000 +0000
@@ -41,12 +41,59 @@
 import os
 import textwrap
 
-import pkg_resources
+import packaging.requirements
 
+from pbr._compat.five import string_type
 from pbr.tests import fixtures as pbr_fixtures
 from pbr.tests.functional import base
 
 
+# Taken from setuptools
+#
+# https://github.com/pypa/setuptools/blob/v44.1.1/pkg_resources/__init__.py#L2378-L2389
+def yield_lines(strs):
+    """Yield non-empty/non-comment lines of a string or sequence"""
+    if isinstance(strs, string_type):
+        for s in strs.splitlines():
+            s = s.strip()
+            # skip blank lines/comments
+            if s and not s.startswith('#'):
+                yield s
+    else:
+        for ss in strs:
+            for s in yield_lines(ss):
+                yield s
+
+
+# Taken from setuptools
+#
+# https://github.com/pypa/setuptools/blob/v44.1.1/pkg_resources/__init__.py#L3189-L3212
+def split_sections(s):
+    """Split a string or iterable thereof into (section, content) pairs
+
+    Each ``section`` is a stripped version of the section header ("[section]")
+    and each ``content`` is a list of stripped lines excluding blank lines and
+    comment-only lines.  If there are any such lines before the first section
+    header, they're returned in a first ``section`` of ``None``.
+    """
+    section = None
+    content = []
+    for line in yield_lines(s):
+        if line.startswith("["):
+            if line.endswith("]"):
+                if section or content:
+                    yield section, content
+                section = line[1:-1].strip()
+                content = []
+            else:
+                raise ValueError("Invalid section heading", line)
+        else:
+            content.append(line)
+
+    # wrap up last segment
+    yield section, content
+
+
 class TestRequirementParsing(base.BaseTestCase):
 
     def test_requirement_parsing(self):
@@ -104,18 +151,23 @@ class TestRequirementParsing(base.BaseTe
 
         requires_txt = os.path.join(egg_info, 'requires.txt')
         with open(requires_txt, 'rt') as requires:
-            generated_requirements = dict(
-                pkg_resources.split_sections(requires)
-            )
+            generated_requirements = dict(split_sections(requires))
 
         # NOTE(dhellmann): We have to spell out the comparison because
         # the rendering for version specifiers in a range is not
         # consistent across versions of setuptools.
 
         for section, expected in expected_requirements.items():
-            exp_parsed = [pkg_resources.Requirement.parse(s) for s in expected]
+            # We wrap in str since we need packaging 22.0.0 or later to do
+            # comparisons [1] and that doesn't support Python 2.7, 3.6
+            #
+            # https://github.com/pypa/packaging/commit/aebc072a06925cc0004b031e6b6f3028e5e2e686
+            # https://pypi.org/project/packaging/22.0/
+            exp_parsed = [
+                str(packaging.requirements.Requirement(s)) for s in expected
+            ]
             gen_parsed = [
-                pkg_resources.Requirement.parse(s)
+                str(packaging.requirements.Requirement(s))
                 for s in generated_requirements[section]
             ]
             self.assertEqual(exp_parsed, gen_parsed)
diff -pruN 7.0.1-3/pbr/tests/test_setupcfg.py 7.0.3-1/pbr/tests/test_setupcfg.py
--- 7.0.1-3/pbr/tests/test_setupcfg.py	1970-01-01 00:00:00.000000000 +0000
+++ 7.0.3-1/pbr/tests/test_setupcfg.py	2025-10-31 18:29:10.000000000 +0000
@@ -0,0 +1,484 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. (HP)
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from __future__ import absolute_import
+from __future__ import print_function
+
+import io
+import os
+import tempfile
+import textwrap
+import warnings
+
+from pbr._compat.five import ConfigParser
+from pbr import setupcfg
+from pbr.tests import base
+
+
+def config_from_ini(ini):
+    config = {}
+    ini = textwrap.dedent(ini)
+    parser = ConfigParser()
+    parser.read_file(io.StringIO(ini))
+    for section in parser.sections():
+        config[section] = dict(parser.items(section))
+    return config
+
+
+class TestBasics(base.BaseTestCase):
+
+    def test_basics(self):
+        self.maxDiff = None
+        config_text = u"""
+            [metadata]
+            name = foo
+            version = 1.0
+            author = John Doe
+            author_email = jd@example.com
+            maintainer = Jim Burke
+            maintainer_email = jb@example.com
+            home_page = http://example.com
+            summary = A foobar project.
+            description = Hello, world. This is a long description.
+            download_url = http://opendev.org/x/pbr
+            classifier =
+                Development Status :: 5 - Production/Stable
+                Programming Language :: Python
+            platform =
+                any
+            license = Apache 2.0
+            requires_dist =
+                Sphinx
+                requests
+            setup_requires_dist =
+                docutils
+            python_requires = >=3.6
+            provides_dist =
+                bax
+            provides_extras =
+                bar
+            obsoletes_dist =
+                baz
+
+            [files]
+            packages_root = src
+            packages =
+                foo
+            package_data =
+                "" = *.txt, *.rst
+                foo = *.msg
+            namespace_packages =
+                hello
+            data_files =
+                bitmaps =
+                    bm/b1.gif
+                    bm/b2.gif
+                config =
+                    cfg/data.cfg
+            scripts =
+                scripts/hello-world.py
+            modules =
+                mod1
+
+            [backwards_compat]
+            zip_safe = true
+            tests_require =
+              fixtures
+            dependency_links =
+              https://example.com/mypackage/v1.2.3.zip#egg=mypackage-1.2.3
+            include_package_data = true
+            """
+        expected = {
+            'name': u'foo',
+            'version': u'1.0',
+            'author': u'John Doe',
+            'author_email': u'jd@example.com',
+            'maintainer': u'Jim Burke',
+            'maintainer_email': u'jb@example.com',
+            'url': u'http://example.com',
+            'description': u'A foobar project.',
+            'long_description': u'Hello, world. This is a long description.',
+            'download_url': u'http://opendev.org/x/pbr',
+            'classifiers': [
+                u'Development Status :: 5 - Production/Stable',
+                u'Programming Language :: Python',
+            ],
+            'platforms': [u'any'],
+            'license': u'Apache 2.0',
+            'install_requires': [
+                u'Sphinx',
+                u'requests',
+            ],
+            'setup_requires': [u'docutils'],
+            'python_requires': u'>=3.6',
+            'provides': [u'bax'],
+            'provides_extras': [u'bar'],
+            'obsoletes': [u'baz'],
+            'extras_require': {},
+            'package_dir': {'': u'src'},
+            'packages': [u'foo'],
+            'package_data': {
+                '': ['*.txt,', '*.rst'],
+                'foo': ['*.msg'],
+            },
+            'namespace_packages': [u'hello'],
+            'data_files': [
+                ('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
+                ('config', ['cfg/data.cfg']),
+            ],
+            'scripts': [u'scripts/hello-world.py'],
+            'py_modules': [u'mod1'],
+            'zip_safe': True,
+            'tests_require': [
+                'fixtures',
+            ],
+            'dependency_links': [
+                'https://example.com/mypackage/v1.2.3.zip#egg=mypackage-1.2.3',
+            ],
+            'include_package_data': True,
+        }
+        config = config_from_ini(config_text)
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            actual = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertDictEqual(expected, actual)
+
+        # split on colon to avoid having to repeat the entire string...
+        warning_messages = set(str(x.message).split(':')[0] for x in w)
+        for warning_message in (
+            "The '[metadata] home_page' option is deprecated",
+            "The '[metadata] summary' option is deprecated",
+            "The '[metadata] classifier' option is deprecated",
+            "The '[metadata] platform' option is deprecated",
+            "The '[metadata] requires_dist' option is deprecated",
+            "The '[metadata] setup_requires_dist' option is deprecated",
+            "The '[metadata] python_requires' option is deprecated",
+            # "The '[metadata] requires_python' option is deprecated",
+            "The '[metadata] provides_dist' option is deprecated",
+            "The '[metadata] provides_extras' option is deprecated",
+            "The '[metadata] obsoletes_dist' option is deprecated",
+            "The '[files] packages' option is deprecated",
+            "The '[files] package_data' option is deprecated",
+            "The '[files] namespace_packages' option is deprecated",
+            "The '[files] data_files' option is deprecated",
+            "The '[files] scripts' option is deprecated",
+            "The '[files] modules' option is deprecated",
+            "The '[backwards_compat] zip_safe' option is deprecated",
+            "The '[backwards_compat] dependency_links' option is deprecated",
+            "The '[backwards_compat] tests_require' option is deprecated",
+            "The '[backwards_compat] include_package_data' option is deprecated",
+        ):
+            self.assertIn(warning_message, warning_messages)
+
+    def test_bug_2120575(self):
+        # check behavior with description, long_description (modern)
+        config_text = u"""
+            [metadata]
+            name = foo
+            description = A short package summary
+            long_description = file: README.rst
+        """
+        expected = {
+            'name': u'foo',
+            'description': u'A short package summary',
+            # long_description should *not* be set: setuptools will handle this
+            # for us
+            'extras_require': {},
+            'install_requires': [],
+        }
+        config = config_from_ini(config_text)
+        actual = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertDictEqual(expected, actual)
+
+        readme = os.path.join(self.temp_dir, 'README.rst')
+        with open(readme, 'w') as f:
+            f.write('A longer summary from the README')
+
+        # check behavior with description, description_file (semi-modern)
+        config_text = (
+            u"""
+            [metadata]
+            name = foo
+            description = A short package summary
+            description_file = %s
+        """
+            % readme
+        )
+        expected = {
+            'name': u'foo',
+            'description': u'A short package summary',
+            'long_description': u'A longer summary from the README\n\n',
+            'extras_require': {},
+            'install_requires': [],
+        }
+        config = config_from_ini(config_text)
+        actual = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertDictEqual(expected, actual)
+
+        # check behavior with summary, long_description (old)
+        config_text = (
+            u"""
+            [metadata]
+            name = foo
+            summary = A short package summary
+            long_description = %s
+        """
+            % readme
+        )
+        expected = {
+            'name': u'foo',
+            'description': u'A short package summary',
+            # long_description is retrieved by setuptools
+            'extras_require': {},
+            'install_requires': [],
+        }
+        config = config_from_ini(config_text)
+        actual = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertDictEqual(expected, actual)
+
+        # check behavior with summary, description_file (ancient)
+        config_text = (
+            u"""
+            [metadata]
+            name = foo
+            summary = A short package summary
+            description_file = %s
+        """
+            % readme
+        )
+        expected = {
+            'name': u'foo',
+            'description': u'A short package summary',
+            'long_description': u'A longer summary from the README\n\n',
+            'extras_require': {},
+            'install_requires': [],
+        }
+        config = config_from_ini(config_text)
+        actual = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertDictEqual(expected, actual)
+
+
+class TestExtrasRequireParsingScenarios(base.BaseTestCase):
+
+    scenarios = [
+        (
+            'simple_extras',
+            {
+                'config_text': u"""
+                [extras]
+                first =
+                    foo
+                    bar==1.0
+                second =
+                    baz>=3.2
+                    foo
+                """,
+                'expected_extra_requires': {
+                    'first': ['foo', 'bar==1.0'],
+                    'second': ['baz>=3.2', 'foo'],
+                    'test': ['requests-mock'],
+                    "test:(python_version=='2.6')": ['ordereddict'],
+                },
+            },
+        ),
+        (
+            'with_markers',
+            {
+                'config_text': u"""
+                [extras]
+                test =
+                    foo:python_version=='2.6'
+                    bar
+                    baz<1.6 :python_version=='2.6'
+                    zaz :python_version>'1.0'
+                """,
+                'expected_extra_requires': {
+                    "test:(python_version=='2.6')": ['foo', 'baz<1.6'],
+                    "test": ['bar', 'zaz'],
+                },
+            },
+        ),
+        (
+            'no_extras',
+            {
+                'config_text': u"""
+            [metadata]
+            long_description = foo
+            """,
+                'expected_extra_requires': {},
+            },
+        ),
+    ]
+
+    def test_extras_parsing(self):
+        config = config_from_ini(self.config_text)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+
+        self.assertEqual(
+            self.expected_extra_requires, kwargs['extras_require']
+        )
+
+
+class TestInvalidMarkers(base.BaseTestCase):
+
+    def test_invalid_marker_raises_error(self):
+        config = {'extras': {'test': "foo :bad_marker>'1.0'"}}
+        self.assertRaises(
+            SyntaxError, setupcfg.setup_cfg_to_setup_kwargs, config
+        )
+
+
+class TestMapFieldsParsingScenarios(base.BaseTestCase):
+
+    scenarios = [
+        (
+            'simple_project_urls',
+            {
+                'config_text': u"""
+                [metadata]
+                project_urls =
+                    Bug Tracker = https://bugs.launchpad.net/pbr/
+                    Documentation = https://docs.openstack.org/pbr/
+                    Source Code = https://opendev.org/openstack/pbr
+                """,  # noqa: E501
+                'expected_project_urls': {
+                    'Bug Tracker': 'https://bugs.launchpad.net/pbr/',
+                    'Documentation': 'https://docs.openstack.org/pbr/',
+                    'Source Code': 'https://opendev.org/openstack/pbr',
+                },
+            },
+        ),
+        (
+            'query_parameters',
+            {
+                'config_text': u"""
+                [metadata]
+                project_urls =
+                    Bug Tracker = https://bugs.launchpad.net/pbr/?query=true
+                    Documentation = https://docs.openstack.org/pbr/?foo=bar
+                    Source Code = https://git.openstack.org/cgit/openstack-dev/pbr/commit/?id=hash
+                """,  # noqa: E501
+                'expected_project_urls': {
+                    'Bug Tracker': 'https://bugs.launchpad.net/pbr/?query=true',
+                    'Documentation': 'https://docs.openstack.org/pbr/?foo=bar',
+                    'Source Code': 'https://git.openstack.org/cgit/openstack-dev/pbr/commit/?id=hash',  # noqa: E501
+                },
+            },
+        ),
+    ]
+
+    def test_project_url_parsing(self):
+        config = config_from_ini(self.config_text)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+
+        self.assertEqual(self.expected_project_urls, kwargs['project_urls'])
+
+
+class TestKeywordsParsingScenarios(base.BaseTestCase):
+
+    scenarios = [
+        (
+            'keywords_list',
+            {
+                'config_text': u"""
+                [metadata]
+                keywords =
+                    one
+                    two
+                    three
+                """,  # noqa: E501
+                'expected_keywords': ['one', 'two', 'three'],
+            },
+        ),
+        (
+            'inline_keywords',
+            {
+                'config_text': u"""
+                [metadata]
+                keywords = one, two, three
+                """,  # noqa: E501
+                'expected_keywords': ['one, two, three'],
+            },
+        ),
+    ]
+
+    def test_keywords_parsing(self):
+        config = config_from_ini(self.config_text)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+
+        self.assertEqual(self.expected_keywords, kwargs['keywords'])
+
+
+class TestProvidesExtras(base.BaseTestCase):
+    def test_provides_extras(self):
+        ini = u"""
+        [metadata]
+        provides_extras = foo
+                          bar
+        """
+        config = config_from_ini(ini)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertEqual(['foo', 'bar'], kwargs['provides_extras'])
+
+
+class TestDataFilesParsing(base.BaseTestCase):
+
+    scenarios = [
+        (
+            'data_files',
+            {
+                'config_text': u"""
+            [files]
+            data_files =
+                'i like spaces/' =
+                    'dir with space/file with spc 2'
+                    'dir with space/file with spc 1'
+            """,
+                'data_files': [
+                    (
+                        'i like spaces/',
+                        [
+                            'dir with space/file with spc 2',
+                            'dir with space/file with spc 1',
+                        ],
+                    )
+                ],
+            },
+        )
+    ]
+
+    def test_handling_of_whitespace_in_data_files(self):
+        config = config_from_ini(self.config_text)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+
+        self.assertEqual(self.data_files, kwargs['data_files'])
+
+
+class TestUTF8DescriptionFile(base.BaseTestCase):
+    def test_utf8_description_file(self):
+        _, path = tempfile.mkstemp()
+        ini_template = u"""
+        [metadata]
+        description_file = %s
+        """
+        # Two \n's because pbr strips the file content and adds \n\n
+        # This way we can use it directly as the assert comparison
+        unicode_description = u'UTF8 description: é"…-ʃŋ\'\n\n'
+        ini = ini_template % path
+        with io.open(path, 'w', encoding='utf8') as f:
+            f.write(unicode_description)
+        config = config_from_ini(ini)
+        kwargs = setupcfg.setup_cfg_to_setup_kwargs(config)
+        self.assertEqual(unicode_description, kwargs['long_description'])
diff -pruN 7.0.1-3/pbr/tests/test_util.py 7.0.3-1/pbr/tests/test_util.py
--- 7.0.1-3/pbr/tests/test_util.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/tests/test_util.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,481 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015 Hewlett-Packard Development Company, L.P. (HP)
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-import io
-import os
-import tempfile
-import textwrap
-import warnings
-
-from pbr._compat.five import ConfigParser
-from pbr.tests import base
-from pbr import util
-
-
-def config_from_ini(ini):
-    config = {}
-    ini = textwrap.dedent(ini)
-    parser = ConfigParser()
-    parser.read_file(io.StringIO(ini))
-    for section in parser.sections():
-        config[section] = dict(parser.items(section))
-    return config
-
-
-class TestBasics(base.BaseTestCase):
-
-    def test_basics(self):
-        self.maxDiff = None
-        config_text = u"""
-            [metadata]
-            name = foo
-            version = 1.0
-            author = John Doe
-            author_email = jd@example.com
-            maintainer = Jim Burke
-            maintainer_email = jb@example.com
-            home_page = http://example.com
-            summary = A foobar project.
-            description = Hello, world. This is a long description.
-            download_url = http://opendev.org/x/pbr
-            classifier =
-                Development Status :: 5 - Production/Stable
-                Programming Language :: Python
-            platform =
-                any
-            license = Apache 2.0
-            requires_dist =
-                Sphinx
-                requests
-            setup_requires_dist =
-                docutils
-            python_requires = >=3.6
-            provides_dist =
-                bax
-            provides_extras =
-                bar
-            obsoletes_dist =
-                baz
-
-            [files]
-            packages_root = src
-            packages =
-                foo
-            package_data =
-                "" = *.txt, *.rst
-                foo = *.msg
-            namespace_packages =
-                hello
-            data_files =
-                bitmaps =
-                    bm/b1.gif
-                    bm/b2.gif
-                config =
-                    cfg/data.cfg
-            scripts =
-                scripts/hello-world.py
-            modules =
-                mod1
-
-            [backwards_compat]
-            zip_safe = true
-            tests_require =
-              fixtures
-            dependency_links =
-              https://example.com/mypackage/v1.2.3.zip#egg=mypackage-1.2.3
-            include_package_data = true
-            """
-        expected = {
-            'name': u'foo',
-            'version': u'1.0',
-            'author': u'John Doe',
-            'author_email': u'jd@example.com',
-            'maintainer': u'Jim Burke',
-            'maintainer_email': u'jb@example.com',
-            'url': u'http://example.com',
-            'description': u'A foobar project.',
-            'long_description': u'Hello, world. This is a long description.',
-            'download_url': u'http://opendev.org/x/pbr',
-            'classifiers': [
-                u'Development Status :: 5 - Production/Stable',
-                u'Programming Language :: Python',
-            ],
-            'platforms': [u'any'],
-            'license': u'Apache 2.0',
-            'install_requires': [
-                u'Sphinx',
-                u'requests',
-            ],
-            'setup_requires': [u'docutils'],
-            'python_requires': u'>=3.6',
-            'provides': [u'bax'],
-            'provides_extras': [u'bar'],
-            'obsoletes': [u'baz'],
-            'extras_require': {},
-            'package_dir': {'': u'src'},
-            'packages': [u'foo'],
-            'package_data': {
-                '': ['*.txt,', '*.rst'],
-                'foo': ['*.msg'],
-            },
-            'namespace_packages': [u'hello'],
-            'data_files': [
-                ('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
-                ('config', ['cfg/data.cfg']),
-            ],
-            'scripts': [u'scripts/hello-world.py'],
-            'py_modules': [u'mod1'],
-            'zip_safe': True,
-            'tests_require': [
-                'fixtures',
-            ],
-            'dependency_links': [
-                'https://example.com/mypackage/v1.2.3.zip#egg=mypackage-1.2.3',
-            ],
-            'include_package_data': True,
-        }
-        config = config_from_ini(config_text)
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-            actual = util.setup_cfg_to_setup_kwargs(config)
-        self.assertDictEqual(expected, actual)
-
-        # split on colon to avoid having to repeat the entire string...
-        warning_messages = set(str(x.message).split(':')[0] for x in w)
-        for warning_message in (
-            "The '[metadata] home_page' option is deprecated",
-            "The '[metadata] summary' option is deprecated",
-            "The '[metadata] classifier' option is deprecated",
-            "The '[metadata] platform' option is deprecated",
-            "The '[metadata] requires_dist' option is deprecated",
-            "The '[metadata] setup_requires_dist' option is deprecated",
-            "The '[metadata] python_requires' option is deprecated",
-            # "The '[metadata] requires_python' option is deprecated",
-            "The '[metadata] provides_dist' option is deprecated",
-            "The '[metadata] provides_extras' option is deprecated",
-            "The '[metadata] obsoletes_dist' option is deprecated",
-            "The '[files] packages' option is deprecated",
-            "The '[files] package_data' option is deprecated",
-            "The '[files] namespace_packages' option is deprecated",
-            "The '[files] data_files' option is deprecated",
-            "The '[files] scripts' option is deprecated",
-            "The '[files] modules' option is deprecated",
-            "The '[backwards_compat] zip_safe' option is deprecated",
-            "The '[backwards_compat] dependency_links' option is deprecated",
-            "The '[backwards_compat] tests_require' option is deprecated",
-            "The '[backwards_compat] include_package_data' option is deprecated",
-        ):
-            self.assertIn(warning_message, warning_messages)
-
-    def test_bug_2120575(self):
-        # check behavior with description, long_description (modern)
-        config_text = u"""
-            [metadata]
-            name = foo
-            description = A short package summary
-            long_description = file: README.rst
-        """
-        expected = {
-            'name': u'foo',
-            'description': u'A short package summary',
-            'long_description': u'file: README.rst',
-            'extras_require': {},
-            'install_requires': [],
-        }
-        config = config_from_ini(config_text)
-        actual = util.setup_cfg_to_setup_kwargs(config)
-        self.assertDictEqual(expected, actual)
-
-        readme = os.path.join(self.temp_dir, 'README.rst')
-        with open(readme, 'w') as f:
-            f.write('A longer summary from the README')
-
-        # check behavior with description, description_file (semi-modern)
-        config_text = (
-            u"""
-            [metadata]
-            name = foo
-            description = A short package summary
-            description_file = %s
-        """
-            % readme
-        )
-        expected = {
-            'name': u'foo',
-            'description': u'A short package summary',
-            'long_description': u'A longer summary from the README\n\n',
-            'extras_require': {},
-            'install_requires': [],
-        }
-        config = config_from_ini(config_text)
-        actual = util.setup_cfg_to_setup_kwargs(config)
-        self.assertDictEqual(expected, actual)
-
-        # check behavior with summary, long_description (old)
-        config_text = (
-            u"""
-            [metadata]
-            name = foo
-            summary = A short package summary
-            long_description = %s
-        """
-            % readme
-        )
-        expected = {
-            'name': u'foo',
-            'description': u'A short package summary',
-            # long_description is retrieved by setuptools
-            'extras_require': {},
-            'install_requires': [],
-        }
-        config = config_from_ini(config_text)
-        actual = util.setup_cfg_to_setup_kwargs(config)
-        self.assertDictEqual(expected, actual)
-
-        # check behavior with summary, description_file (ancient)
-        config_text = (
-            u"""
-            [metadata]
-            name = foo
-            summary = A short package summary
-            description_file = %s
-        """
-            % readme
-        )
-        expected = {
-            'name': u'foo',
-            'description': u'A short package summary',
-            'long_description': u'A longer summary from the README\n\n',
-            'extras_require': {},
-            'install_requires': [],
-        }
-        config = config_from_ini(config_text)
-        actual = util.setup_cfg_to_setup_kwargs(config)
-        self.assertDictEqual(expected, actual)
-
-
-class TestExtrasRequireParsingScenarios(base.BaseTestCase):
-
-    scenarios = [
-        (
-            'simple_extras',
-            {
-                'config_text': u"""
-                [extras]
-                first =
-                    foo
-                    bar==1.0
-                second =
-                    baz>=3.2
-                    foo
-                """,
-                'expected_extra_requires': {
-                    'first': ['foo', 'bar==1.0'],
-                    'second': ['baz>=3.2', 'foo'],
-                    'test': ['requests-mock'],
-                    "test:(python_version=='2.6')": ['ordereddict'],
-                },
-            },
-        ),
-        (
-            'with_markers',
-            {
-                'config_text': u"""
-                [extras]
-                test =
-                    foo:python_version=='2.6'
-                    bar
-                    baz<1.6 :python_version=='2.6'
-                    zaz :python_version>'1.0'
-                """,
-                'expected_extra_requires': {
-                    "test:(python_version=='2.6')": ['foo', 'baz<1.6'],
-                    "test": ['bar', 'zaz'],
-                },
-            },
-        ),
-        (
-            'no_extras',
-            {
-                'config_text': u"""
-            [metadata]
-            long_description = foo
-            """,
-                'expected_extra_requires': {},
-            },
-        ),
-    ]
-
-    def test_extras_parsing(self):
-        config = config_from_ini(self.config_text)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-
-        self.assertEqual(
-            self.expected_extra_requires, kwargs['extras_require']
-        )
-
-
-class TestInvalidMarkers(base.BaseTestCase):
-
-    def test_invalid_marker_raises_error(self):
-        config = {'extras': {'test': "foo :bad_marker>'1.0'"}}
-        self.assertRaises(SyntaxError, util.setup_cfg_to_setup_kwargs, config)
-
-
-class TestMapFieldsParsingScenarios(base.BaseTestCase):
-
-    scenarios = [
-        (
-            'simple_project_urls',
-            {
-                'config_text': u"""
-                [metadata]
-                project_urls =
-                    Bug Tracker = https://bugs.launchpad.net/pbr/
-                    Documentation = https://docs.openstack.org/pbr/
-                    Source Code = https://opendev.org/openstack/pbr
-                """,  # noqa: E501
-                'expected_project_urls': {
-                    'Bug Tracker': 'https://bugs.launchpad.net/pbr/',
-                    'Documentation': 'https://docs.openstack.org/pbr/',
-                    'Source Code': 'https://opendev.org/openstack/pbr',
-                },
-            },
-        ),
-        (
-            'query_parameters',
-            {
-                'config_text': u"""
-                [metadata]
-                project_urls =
-                    Bug Tracker = https://bugs.launchpad.net/pbr/?query=true
-                    Documentation = https://docs.openstack.org/pbr/?foo=bar
-                    Source Code = https://git.openstack.org/cgit/openstack-dev/pbr/commit/?id=hash
-                """,  # noqa: E501
-                'expected_project_urls': {
-                    'Bug Tracker': 'https://bugs.launchpad.net/pbr/?query=true',
-                    'Documentation': 'https://docs.openstack.org/pbr/?foo=bar',
-                    'Source Code': 'https://git.openstack.org/cgit/openstack-dev/pbr/commit/?id=hash',  # noqa: E501
-                },
-            },
-        ),
-    ]
-
-    def test_project_url_parsing(self):
-        config = config_from_ini(self.config_text)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-
-        self.assertEqual(self.expected_project_urls, kwargs['project_urls'])
-
-
-class TestKeywordsParsingScenarios(base.BaseTestCase):
-
-    scenarios = [
-        (
-            'keywords_list',
-            {
-                'config_text': u"""
-                [metadata]
-                keywords =
-                    one
-                    two
-                    three
-                """,  # noqa: E501
-                'expected_keywords': ['one', 'two', 'three'],
-            },
-        ),
-        (
-            'inline_keywords',
-            {
-                'config_text': u"""
-                [metadata]
-                keywords = one, two, three
-                """,  # noqa: E501
-                'expected_keywords': ['one, two, three'],
-            },
-        ),
-    ]
-
-    def test_keywords_parsing(self):
-        config = config_from_ini(self.config_text)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-
-        self.assertEqual(self.expected_keywords, kwargs['keywords'])
-
-
-class TestProvidesExtras(base.BaseTestCase):
-    def test_provides_extras(self):
-        ini = u"""
-        [metadata]
-        provides_extras = foo
-                          bar
-        """
-        config = config_from_ini(ini)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-        self.assertEqual(['foo', 'bar'], kwargs['provides_extras'])
-
-
-class TestDataFilesParsing(base.BaseTestCase):
-
-    scenarios = [
-        (
-            'data_files',
-            {
-                'config_text': u"""
-            [files]
-            data_files =
-                'i like spaces/' =
-                    'dir with space/file with spc 2'
-                    'dir with space/file with spc 1'
-            """,
-                'data_files': [
-                    (
-                        'i like spaces/',
-                        [
-                            'dir with space/file with spc 2',
-                            'dir with space/file with spc 1',
-                        ],
-                    )
-                ],
-            },
-        )
-    ]
-
-    def test_handling_of_whitespace_in_data_files(self):
-        config = config_from_ini(self.config_text)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-
-        self.assertEqual(self.data_files, kwargs['data_files'])
-
-
-class TestUTF8DescriptionFile(base.BaseTestCase):
-    def test_utf8_description_file(self):
-        _, path = tempfile.mkstemp()
-        ini_template = u"""
-        [metadata]
-        description_file = %s
-        """
-        # Two \n's because pbr strips the file content and adds \n\n
-        # This way we can use it directly as the assert comparison
-        unicode_description = u'UTF8 description: é"…-ʃŋ\'\n\n'
-        ini = ini_template % path
-        with io.open(path, 'w', encoding='utf8') as f:
-            f.write(unicode_description)
-        config = config_from_ini(ini)
-        kwargs = util.setup_cfg_to_setup_kwargs(config)
-        self.assertEqual(unicode_description, kwargs['long_description'])
diff -pruN 7.0.1-3/pbr/util.py 7.0.3-1/pbr/util.py
--- 7.0.1-3/pbr/util.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/pbr/util.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,754 +0,0 @@
-# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#    http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-# implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# Copyright (C) 2013 Association of Universities for Research in Astronomy
-#                    (AURA)
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-#     1. Redistributions of source code must retain the above copyright
-#        notice, this list of conditions and the following disclaimer.
-#
-#     2. Redistributions in binary form must reproduce the above
-#        copyright notice, this list of conditions and the following
-#        disclaimer in the documentation and/or other materials provided
-#        with the distribution.
-#
-#     3. The name of AURA and its representatives may not be used to
-#        endorse or promote products derived from this software without
-#        specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY AURA ``AS IS'' AND ANY EXPRESS OR IMPLIED
-# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL AURA BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
-# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
-# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
-# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
-# DAMAGE.
-
-# The code in this module is mostly copy/pasted out of the distutils2 source
-# code, as recommended by Tarek Ziade.
-
-from __future__ import absolute_import
-from __future__ import print_function
-
-# These first two imports are not used, but are needed to get around an
-# irritating Python bug that can crop up when using ./setup.py test.
-# See: http://www.eby-sarna.com/pipermail/peak/2010-May/003355.html
-try:
-    import multiprocessing  # noqa
-except ImportError:
-    pass
-import logging  # noqa
-
-from collections import defaultdict
-import io
-import os
-import re
-import shlex
-import sys
-import traceback
-import warnings
-
-from distutils import errors
-from distutils import log
-import setuptools
-from setuptools import dist as st_dist
-from setuptools import extension
-
-from pbr._compat.five import ConfigParser
-import pbr._compat.packaging
-from pbr import extra_files
-import pbr.hooks
-
-# A simplified RE for this; just checks that the line ends with version
-# predicates in ()
-_VERSION_SPEC_RE = re.compile(r'\s*(.*?)\s*\((.*)\)\s*$')
-
-# Mappings from setup.cfg options, in (section, option) form, to setup()
-# keyword arguments
-CFG_TO_PY_SETUP_ARGS = (
-    (('metadata', 'name'), 'name'),
-    (('metadata', 'version'), 'version'),
-    (('metadata', 'author'), 'author'),
-    (('metadata', 'author_email'), 'author_email'),
-    (('metadata', 'maintainer'), 'maintainer'),
-    (('metadata', 'maintainer_email'), 'maintainer_email'),
-    (('metadata', 'home_page'), 'url'),
-    (('metadata', 'project_urls'), 'project_urls'),
-    (('metadata', 'summary'), 'description'),
-    (('metadata', 'keywords'), 'keywords'),
-    (('metadata', 'description'), 'long_description'),
-    (
-        ('metadata', 'description_content_type'),
-        'long_description_content_type',
-    ),
-    (('metadata', 'download_url'), 'download_url'),
-    (('metadata', 'classifier'), 'classifiers'),
-    (('metadata', 'platform'), 'platforms'),  # **
-    (('metadata', 'license'), 'license'),
-    # Use setuptools install_requires, not
-    # broken distutils requires
-    (('metadata', 'requires_dist'), 'install_requires'),
-    (('metadata', 'setup_requires_dist'), 'setup_requires'),
-    (('metadata', 'python_requires'), 'python_requires'),
-    (('metadata', 'requires_python'), 'python_requires'),
-    (('metadata', 'provides_dist'), 'provides'),  # **
-    (('metadata', 'provides_extras'), 'provides_extras'),
-    (('metadata', 'obsoletes_dist'), 'obsoletes'),  # **
-    (('files', 'packages_root'), 'package_dir'),
-    (('files', 'packages'), 'packages'),
-    (('files', 'package_data'), 'package_data'),
-    (('files', 'namespace_packages'), 'namespace_packages'),
-    (('files', 'data_files'), 'data_files'),
-    (('files', 'scripts'), 'scripts'),
-    (('files', 'modules'), 'py_modules'),  # **
-    (('global', 'commands'), 'cmdclass'),
-    # Not supported in distutils2, but provided for
-    # backwards compatibility with setuptools
-    (('backwards_compat', 'zip_safe'), 'zip_safe'),
-    (('backwards_compat', 'tests_require'), 'tests_require'),
-    (('backwards_compat', 'dependency_links'), 'dependency_links'),
-    (('backwards_compat', 'include_package_data'), 'include_package_data'),
-)
-
-DEPRECATED_CFG = {
-    ('metadata', 'home_page'): (
-        "Use '[metadata] url' (setup.cfg) or '[project.urls]' "
-        "(pyproject.toml) instead"
-    ),
-    ('metadata', 'summary'): (
-        "Use '[metadata] description' (setup.cfg) or '[project] description' "
-        "(pyproject.toml) instead"
-    ),
-    ('metadata', 'description_file'): (
-        "Use '[metadata] long_description' (setup.cfg) or '[project] readme' "
-        "(pyproject.toml) instead"
-    ),
-    ('metadata', 'classifier'): (
-        "Use '[metadata] classifiers' (setup.cfg) or '[project] classifiers' "
-        "(pyproject.toml) instead"
-    ),
-    ('metadata', 'platform'): (
-        "Use '[metadata] platforms' (setup.cfg) or "
-        "'[tool.setuptools] platforms' (pyproject.toml) instead"
-    ),
-    ('metadata', 'requires_dist'): (
-        "Use '[options] install_requires' (setup.cfg) or "
-        "'[project] dependencies' (pyproject.toml) instead"
-    ),
-    ('metadata', 'setup_requires_dist'): (
-        "Use '[options] setup_requires' (setup.cfg) or "
-        "'[build-system] requires' (pyproject.toml) instead"
-    ),
-    ('metadata', 'python_requires'): (
-        "Use '[options] python_requires' (setup.cfg) or "
-        "'[project] requires-python' (pyproject.toml) instead"
-    ),
-    ('metadata', 'requires_python'): (
-        "Use '[options] python_requires' (setup.cfg) or "
-        "'[project] requires-python' (pyproject.toml) instead"
-    ),
-    ('metadata', 'provides_dist'): "This option is ignored by pip",
-    ('metadata', 'provides_extras'): "This option is ignored by pip",
-    ('metadata', 'obsoletes_dist'): "This option is ignored by pip",
-    ('files', 'packages_root'): (
-        "Use '[options] package_dir' (setup.cfg) or '[tools.setuptools] "
-        "package_dir' (pyproject.toml) instead"
-    ),
-    ('files', 'packages'): (
-        "Use '[options] packages' (setup.cfg) or '[tools.setuptools] "
-        "packages' (pyproject.toml) instead"
-    ),
-    ('files', 'package_data'): (
-        "Use '[options.package_data]' (setup.cfg) or "
-        "'[tool.setuptools.package-data]' (pyproject.toml) instead"
-    ),
-    ('files', 'namespace_packages'): (
-        "Use '[options] namespace_packages' (setup.cfg) or migrate to PEP "
-        "420-style namespace packages instead"
-    ),
-    ('files', 'data_files'): (
-        "For package data files, use '[options] package_data' (setup.cfg) "
-        "or '[tools.setuptools] package_data' (pyproject.toml) instead. "
-        "Support for non-package data files is deprecated in setuptools "
-        "and their use is discouraged. If necessary, use "
-        "'[options] data_files' (setup.cfg) or '[tools.setuptools] data-files'"
-        "(pyproject.toml) instead."
-    ),
-    ('files', 'scripts'): (
-        "Migrate to using the console_scripts entrypoint and use "
-        "'[options.entry_points]' (setup.cfg) or '[project.scripts]' "
-        "(pyproject.toml) instead"
-    ),
-    ('files', 'modules'): (
-        "Use '[options] py_modules' (setup.cfg) or '[tools.setuptools] "
-        "py-modules' (pyproject.toml) instead"
-    ),
-    ('backwards_compat', 'zip_safe'): (
-        "This option is obsolete as it was only relevant in the context of "
-        "eggs"
-    ),
-    ('backwards_compat', 'dependency_links'): (
-        "This option is ignored by pip starting from pip 19.0"
-    ),
-    ('backwards_compat', 'tests_require'): (
-        "This option is ignored by pip starting from pip 19.0"
-    ),
-    ('backwards_compat', 'include_package_data'): (
-        "Use '[options] include_package_data' (setup.cfg) or "
-        "'[tools.setuptools] include-package-data' (pyproject.toml) instead"
-    ),
-}
-
-# setup() arguments that can have multiple values in setup.cfg
-MULTI_FIELDS = (
-    "classifiers",
-    "platforms",
-    "install_requires",
-    "provides",
-    "obsoletes",
-    "namespace_packages",
-    "packages",
-    "package_data",
-    "data_files",
-    "scripts",
-    "py_modules",
-    "dependency_links",
-    "setup_requires",
-    "tests_require",
-    "keywords",
-    "cmdclass",
-    "provides_extras",
-)
-
-# a mapping of removed keywords to the version of setuptools that they were deprecated in
-REMOVED_KEYWORDS = {
-    # https://setuptools.pypa.io/en/stable/history.html#v72-0-0
-    'tests_requires': '72.0.0',
-}
-
-# setup() arguments that can have mapping values in setup.cfg
-MAP_FIELDS = ("project_urls",)
-
-# setup() arguments that contain boolean values
-BOOL_FIELDS = ("zip_safe", "include_package_data")
-
-CSV_FIELDS = ()
-
-
-def shlex_split(path):
-    if os.name == 'nt':
-        # shlex cannot handle paths that contain backslashes, treating those
-        # as escape characters.
-        path = path.replace("\\", "/")
-        return [x.replace("/", "\\") for x in shlex.split(path)]
-
-    return shlex.split(path)
-
-
-def resolve_name(name):
-    """Resolve a name like ``module.object`` to an object and return it.
-
-    Raise ImportError if the module or name is not found.
-    """
-
-    parts = name.split('.')
-    cursor = len(parts) - 1
-    module_name = parts[:cursor]
-    attr_name = parts[-1]
-
-    while cursor > 0:
-        try:
-            ret = __import__('.'.join(module_name), fromlist=[attr_name])
-            break
-        except ImportError:
-            if cursor == 0:
-                raise
-            cursor -= 1
-            module_name = parts[:cursor]
-            attr_name = parts[cursor]
-            ret = ''
-
-    for part in parts[cursor:]:
-        try:
-            ret = getattr(ret, part)
-        except AttributeError:
-            raise ImportError(name)
-
-    return ret
-
-
-def cfg_to_args(path='setup.cfg', script_args=()):
-    """Distutils2 to distutils1 compatibility util.
-
-    This method uses an existing setup.cfg to generate a dictionary of
-    keywords that can be used by distutils.core.setup(kwargs**).
-
-    :param path:
-        The setup.cfg path.
-    :param script_args:
-        List of commands setup.py was called with.
-    :raises DistutilsFileError:
-        When the setup.cfg file is not found.
-    """
-    # The method source code really starts here.
-    parser = ConfigParser()
-
-    if not os.path.exists(path):
-        raise errors.DistutilsFileError(
-            "file '%s' does not exist" % os.path.abspath(path)
-        )
-    try:
-        parser.read(path, encoding='utf-8')
-    except TypeError:
-        # Python 2 doesn't accept the encoding kwarg
-        parser.read(path)
-    config = {}
-    for section in parser.sections():
-        config[section] = {}
-        for k, value in parser.items(section):
-            config[section][k.replace('-', '_')] = value
-
-    # Run setup_hooks, if configured
-    setup_hooks = has_get_option(config, 'global', 'setup_hooks')
-    package_dir = has_get_option(config, 'files', 'packages_root')
-
-    # Add the source package directory to sys.path in case it contains
-    # additional hooks, and to make sure it's on the path before any existing
-    # installations of the package
-    if package_dir:
-        package_dir = os.path.abspath(package_dir)
-        sys.path.insert(0, package_dir)
-
-    try:
-        if setup_hooks:
-            setup_hooks = [
-                hook
-                for hook in split_multiline(setup_hooks)
-                if hook != 'pbr.hooks.setup_hook'
-            ]
-            for hook in setup_hooks:
-                hook_fn = resolve_name(hook)
-                try:
-                    hook_fn(config)
-                except SystemExit:
-                    log.error('setup hook %s terminated the installation')
-                except Exception:
-                    e = sys.exc_info()[1]
-                    log.error(
-                        'setup hook %s raised exception: %s\n' % (hook, e)
-                    )
-                    log.error(traceback.format_exc())
-                    sys.exit(1)
-
-        # Run the pbr hook
-        pbr.hooks.setup_hook(config)
-
-        kwargs = setup_cfg_to_setup_kwargs(config, script_args)
-
-        # Set default config overrides
-        kwargs['include_package_data'] = True
-        kwargs['zip_safe'] = False
-
-        if has_get_option(config, 'global', 'compilers'):
-            warnings.warn(
-                'Support for custom compilers was removed in pbr 7.0 and the '
-                '\'[global] compilers\' option is now ignored.',
-                DeprecationWarning,
-            )
-
-        ext_modules = get_extension_modules(config)
-        if ext_modules:
-            kwargs['ext_modules'] = ext_modules
-
-        entry_points = get_entry_points(config)
-        if entry_points:
-            kwargs['entry_points'] = entry_points
-
-        # Handle the [files]/extra_files option
-        files_extra_files = has_get_option(config, 'files', 'extra_files')
-        if files_extra_files:
-            extra_files.set_extra_files(split_multiline(files_extra_files))
-
-    finally:
-        # Perform cleanup if any paths were added to sys.path
-        if package_dir:
-            sys.path.pop(0)
-
-    return kwargs
-
-
-def _read_description_file(config):
-    """Handle the legacy 'description_file' option."""
-    description_files = has_get_option(config, 'metadata', 'description_file')
-    if not description_files:
-        return None
-
-    description_files = split_multiline(description_files)
-
-    data = ''
-    for filename in description_files:
-        description_file = io.open(filename, encoding='utf-8')
-        try:
-            data += description_file.read().strip() + '\n\n'
-        finally:
-            description_file.close()
-
-    return data
-
-
-def setup_cfg_to_setup_kwargs(config, script_args=()):
-    """Convert config options to kwargs.
-
-    Processes the setup.cfg options and converts them to arguments accepted
-    by setuptools' setup() function.
-    """
-
-    kwargs = {}
-
-    # Temporarily holds install_requires and extra_requires while we
-    # parse env_markers.
-    all_requirements = {}
-
-    # We want people to use description and long_description over summary and
-    # description but there is obvious overlap. If we see the both of the
-    # former being used, don't normalize
-    skip_description_normalization = False
-    if has_get_option(config, 'metadata', 'description') and (
-        has_get_option(config, 'metadata', 'long_description')
-        or has_get_option(config, 'metadata', 'description_file')
-    ):
-        kwargs['description'] = has_get_option(
-            config, 'metadata', 'description'
-        )
-        long_description = has_get_option(
-            config, 'metadata', 'long_description'
-        )
-        if long_description:
-            kwargs['long_description'] = long_description
-        else:
-            kwargs['long_description'] = _read_description_file(config)
-
-        skip_description_normalization = True
-
-    for alias, arg in CFG_TO_PY_SETUP_ARGS:
-        section, option = alias
-
-        if skip_description_normalization and alias in (
-            ('metadata', 'summary'),
-            ('metadata', 'description'),
-        ):
-            continue
-
-        in_cfg_value = has_get_option(config, section, option)
-
-        if alias == ('metadata', 'description') and not in_cfg_value:
-            in_cfg_value = _read_description_file(config)
-
-        if not in_cfg_value:
-            continue
-
-        if alias in DEPRECATED_CFG:
-            warnings.warn(
-                "The '[%s] %s' option is deprecated: %s"
-                % (alias[0], alias[1], DEPRECATED_CFG[alias]),
-                DeprecationWarning,
-            )
-
-        if arg in CSV_FIELDS:
-            in_cfg_value = split_csv(in_cfg_value)
-
-        if arg in MULTI_FIELDS:
-            in_cfg_value = split_multiline(in_cfg_value)
-        elif arg in MAP_FIELDS:
-            in_cfg_map = {}
-            for i in split_multiline(in_cfg_value):
-                k, v = i.split('=', 1)
-                in_cfg_map[k.strip()] = v.strip()
-            in_cfg_value = in_cfg_map
-        elif arg in BOOL_FIELDS:
-            # Provide some flexibility here...
-            if in_cfg_value.lower() in ('true', 't', '1', 'yes', 'y'):
-                in_cfg_value = True
-            else:
-                in_cfg_value = False
-
-        if in_cfg_value:
-            if arg in REMOVED_KEYWORDS and (
-                pbr._compat.packaging.parse_version(setuptools.__version__)
-                >= pbr._compat.packaging.parse_version(REMOVED_KEYWORDS[arg])
-            ):
-                # deprecation warnings, if any, will already have been logged,
-                # so simply skip this
-                continue
-
-            if arg in ('install_requires', 'tests_require'):
-                # Replaces PEP345-style version specs with the sort expected by
-                # setuptools
-                in_cfg_value = [
-                    _VERSION_SPEC_RE.sub(r'\1\2', pred)
-                    for pred in in_cfg_value
-                ]
-            if arg == 'install_requires':
-                # Split install_requires into package,env_marker tuples
-                # These will be re-assembled later
-                install_requires = []
-                requirement_pattern = (
-                    r'(?P<package>[^;]*);?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
-                )
-                for requirement in in_cfg_value:
-                    m = re.match(requirement_pattern, requirement)
-                    requirement_package = m.group('package').strip()
-                    env_marker = m.group('env_marker').strip()
-                    install_requires.append((requirement_package, env_marker))
-                all_requirements[''] = install_requires
-            elif arg == 'package_dir':
-                in_cfg_value = {'': in_cfg_value}
-            elif arg in ('package_data', 'data_files'):
-                data_files = {}
-                firstline = True
-                prev = None
-                for line in in_cfg_value:
-                    if '=' in line:
-                        key, value = line.split('=', 1)
-                        key_unquoted = shlex_split(key.strip())[0]
-                        key, value = (key_unquoted, value.strip())
-                        if key in data_files:
-                            # Multiple duplicates of the same package name;
-                            # this is for backwards compatibility of the old
-                            # format prior to d2to1 0.2.6.
-                            prev = data_files[key]
-                            prev.extend(shlex_split(value))
-                        else:
-                            prev = data_files[key.strip()] = shlex_split(value)
-                    elif firstline:
-                        raise errors.DistutilsOptionError(
-                            'malformed package_data first line %r (misses '
-                            '"=")' % line
-                        )
-                    else:
-                        prev.extend(shlex_split(line.strip()))
-                    firstline = False
-                if arg == 'data_files':
-                    # the data_files value is a pointlessly different structure
-                    # from the package_data value
-                    data_files = sorted(data_files.items())
-                in_cfg_value = data_files
-            elif arg == 'cmdclass':
-                cmdclass = {}
-                dist = st_dist.Distribution()
-                for cls_name in in_cfg_value:
-                    cls = resolve_name(cls_name)
-                    cmd = cls(dist)
-                    cmdclass[cmd.get_command_name()] = cls
-                in_cfg_value = cmdclass
-
-        kwargs[arg] = in_cfg_value
-
-    # Transform requirements with embedded environment markers to
-    # setuptools' supported marker-per-requirement format.
-    #
-    # install_requires are treated as a special case of extras, before
-    # being put back in the expected place
-    #
-    # fred =
-    #     foo:marker
-    #     bar
-    # -> {'fred': ['bar'], 'fred:marker':['foo']}
-
-    if 'extras' in config:
-        requirement_pattern = (
-            r'(?P<package>[^:]*):?(?P<env_marker>[^#]*?)(?:\s*#.*)?$'
-        )
-        extras = config['extras']
-        # Add contents of test-requirements, if any, into an extra named
-        # 'test' if one does not already exist.
-        if 'test' not in extras:
-            from pbr import packaging
-
-            extras['test'] = "\n".join(
-                packaging.parse_requirements(packaging.TEST_REQUIREMENTS_FILES)
-            ).replace(';', ':')
-
-        for extra in extras:
-            extra_requirements = []
-            requirements = split_multiline(extras[extra])
-            for requirement in requirements:
-                m = re.match(requirement_pattern, requirement)
-                extras_value = m.group('package').strip()
-                env_marker = m.group('env_marker')
-                extra_requirements.append((extras_value, env_marker))
-            all_requirements[extra] = extra_requirements
-
-    # Transform the full list of requirements into:
-    # - install_requires, for those that have no extra and no
-    #   env_marker
-    # - named extras, for those with an extra name (which may include
-    #   an env_marker)
-    # - and as a special case, install_requires with an env_marker are
-    #   treated as named extras where the name is the empty string
-
-    extras_require = {}
-    for req_group in all_requirements:
-        for requirement, env_marker in all_requirements[req_group]:
-            if env_marker:
-                extras_key = '%s:(%s)' % (req_group, env_marker)
-                # We do not want to poison wheel creation with locally
-                # evaluated markers.  sdists always re-create the egg_info
-                # and as such do not need guarded, and pip will never call
-                # multiple setup.py commands at once.
-                if 'bdist_wheel' not in script_args:
-                    try:
-                        if pbr._compat.packaging.evaluate_marker(
-                            '(%s)' % env_marker
-                        ):
-                            extras_key = req_group
-                    except SyntaxError:
-                        log.error(
-                            "Marker evaluation failed, see the following "
-                            "error.  For more information see: "
-                            "http://docs.openstack.org/"
-                            "pbr/latest/user/using.html#environment-markers"
-                        )
-                        raise
-            else:
-                extras_key = req_group
-            extras_require.setdefault(extras_key, []).append(requirement)
-
-    kwargs['install_requires'] = extras_require.pop('', [])
-    kwargs['extras_require'] = extras_require
-
-    return kwargs
-
-
-def get_extension_modules(config):
-    """Handle extension modules"""
-
-    EXTENSION_FIELDS = (
-        "sources",
-        "include_dirs",
-        "define_macros",
-        "undef_macros",
-        "library_dirs",
-        "libraries",
-        "runtime_library_dirs",
-        "extra_objects",
-        "extra_compile_args",
-        "extra_link_args",
-        "export_symbols",
-        "swig_opts",
-        "depends",
-    )
-
-    ext_modules = []
-    for section in config:
-        if ':' in section:
-            labels = section.split(':', 1)
-        else:
-            # Backwards compatibility for old syntax; don't use this though
-            labels = section.split('=', 1)
-        labels = [label.strip() for label in labels]
-        if (len(labels) == 2) and (labels[0] == 'extension'):
-            ext_args = {}
-            for field in EXTENSION_FIELDS:
-                value = has_get_option(config, section, field)
-                # All extension module options besides name can have multiple
-                # values
-                if not value:
-                    continue
-                value = split_multiline(value)
-                if field == 'define_macros':
-                    macros = []
-                    for macro in value:
-                        macro = macro.split('=', 1)
-                        if len(macro) == 1:
-                            macro = (macro[0].strip(), None)
-                        else:
-                            macro = (macro[0].strip(), macro[1].strip())
-                        macros.append(macro)
-                    value = macros
-                ext_args[field] = value
-            if ext_args:
-                if 'name' not in ext_args:
-                    ext_args['name'] = labels[1]
-                ext_modules.append(
-                    extension.Extension(ext_args.pop('name'), **ext_args)
-                )
-    return ext_modules
-
-
-def get_entry_points(config):
-    """Process the [entry_points] section of setup.cfg."""
-
-    if 'entry_points' not in config:
-        return {}
-
-    warnings.warn(
-        "The 'entry_points' section has been deprecated in favour of the "
-        "'[options.entry_points]' section (if using 'setup.cfg') or the "
-        "'[project.scripts]' and/or '[project.entry-points.{name}]' sections "
-        "(if using 'pyproject.toml')",
-        DeprecationWarning,
-    )
-
-    return {
-        option: split_multiline(value)
-        for option, value in config['entry_points'].items()
-    }
-
-
-def has_get_option(config, section, option):
-    if section in config and option in config[section]:
-        return config[section][option]
-    else:
-        return False
-
-
-def split_multiline(value):
-    """Special behaviour when we have a multi line options"""
-
-    value = [
-        element
-        for element in (line.strip() for line in value.split('\n'))
-        if element and not element.startswith('#')
-    ]
-    return value
-
-
-def split_csv(value):
-    """Special behaviour when we have a comma separated options"""
-
-    value = [
-        element
-        for element in (chunk.strip() for chunk in value.split(','))
-        if element
-    ]
-    return value
-
-
-# The following classes are used to hack Distribution.command_options a bit
-class DefaultGetDict(defaultdict):
-    """Like defaultdict, but get() also sets and returns the default value."""
-
-    def get(self, key, default=None):
-        if default is None:
-            default = self.default_factory()
-        return super(DefaultGetDict, self).setdefault(key, default)
diff -pruN 7.0.1-3/setup.cfg 7.0.3-1/setup.cfg
--- 7.0.1-3/setup.cfg	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/setup.cfg	2025-10-31 18:29:10.000000000 +0000
@@ -37,7 +37,7 @@ packages =
 
 [options.entry_points]
 distutils.setup_keywords =
-    pbr = pbr.core:pbr
+    pbr = pbr.setupcfg:pbr
 egg_info.writers =
     pbr.json = pbr.pbr_json:write_pbr_json
 console_scripts =
diff -pruN 7.0.1-3/setup.py 7.0.3-1/setup.py
--- 7.0.1-3/setup.py	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/setup.py	2025-10-31 18:29:10.000000000 +0000
@@ -15,6 +15,6 @@
 
 import setuptools
 
-from pbr import util
+from pbr import setupcfg
 
-setuptools.setup(**util.cfg_to_args())
+setuptools.setup(**setupcfg.setup_cfg_to_args())
diff -pruN 7.0.1-3/test-requirements.txt 7.0.3-1/test-requirements.txt
--- 7.0.1-3/test-requirements.txt	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/test-requirements.txt	2025-10-31 18:29:10.000000000 +0000
@@ -1,6 +1,13 @@
+# The order of packages is significant on Python 2.7, because older pip
+# versions (before pip v20.3) process them in the order of appearance. Changing
+# the order has an impact on the overall integration process, which may cause
+# wedges in the gate later.
+
 wheel>=0.32.0 # MIT
 fixtures>=3.0.0 # Apache-2.0/BSD
 mock>=2.0.0,<4.0.0;python_version=='2.7' # BSD
+# indirect dependency (via stestr) with broken Python 2.7 support
+pyperclip<1.10.0;python_version=='2.7' # BSD
 stestr>=2.1.0,<3.0;python_version=='2.7' # Apache-2.0
 stestr>=2.1.0;python_version>='3.0' # Apache-2.0
 testresources>=2.0.0 # Apache-2.0/BSD
@@ -8,6 +15,7 @@ testscenarios>=0.4 # Apache-2.0/BSD
 testtools>=2.2.0 # MIT
 virtualenv>=20.0.3 # MIT
 coverage!=4.4,>=4.0 # Apache-2.0
+packaging>=20.0 # Apache-2.0
 
 # optionally exposed by distutils commands
 sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD
diff -pruN 7.0.1-3/tox.ini 7.0.3-1/tox.ini
--- 7.0.1-3/tox.ini	2025-08-14 16:07:35.000000000 +0000
+++ 7.0.3-1/tox.ini	2025-10-31 18:29:10.000000000 +0000
@@ -79,3 +79,5 @@ commands =
 ignore = E203,E501,W503,H216
 exclude = .venv,.tox,dist,doc,*.egg,build
 show-source = true
+per-file-ignores =
+  pbr/_compat/easy_install.py:H404,H405
