Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-oslo.config for 
openSUSE:Factory checked in at 2026-08-09 21:34:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-oslo.config (Old)
 and      /work/SRC/openSUSE:Factory/.python-oslo.config.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-oslo.config"

Sun Aug  9 21:34:46 2026 rev:52 rq:1369779 version:10.6.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-oslo.config/python-oslo.config.changes    
2026-06-15 19:55:04.160668481 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-oslo.config.new.16738/python-oslo.config.changes
 2026-08-09 21:37:04.885801164 +0200
@@ -1,0 +2,14 @@
+Wed Aug  5 14:25:20 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 10.6.0:
+  * Name ConfigOpts serialized state version
+  * ruff: Enable log-related checks
+  * Support ConfigOpts state snapshots for spawn workers
+  * zuul: Use openstack-python3-next-jobs template
+  * pre-commit: Bump versions
+  * Run doc8
+  * Do not install code to build release notes
+  * Loose validation for schemes
+  * Better support Sphinx doc generation with custom option types
+
+-------------------------------------------------------------------

Old:
----
  oslo_config-10.5.0.tar.gz

New:
----
  oslo_config-10.6.0.tar.gz

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

Other differences:
------------------
++++++ python-oslo.config.spec ++++++
--- /var/tmp/diff_new_pack.6viotX/_old  2026-08-09 21:37:05.341816691 +0200
+++ /var/tmp/diff_new_pack.6viotX/_new  2026-08-09 21:37:05.341816691 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-oslo.config
-Version:        10.5.0
+Version:        10.6.0
 Release:        0
 Summary:        OpenStack common configuration library
 License:        Apache-2.0

++++++ oslo_config-10.5.0.tar.gz -> oslo_config-10.6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/.pre-commit-config.yaml 
new/oslo_config-10.6.0/.pre-commit-config.yaml
--- old/oslo_config-10.5.0/.pre-commit-config.yaml      2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/.pre-commit-config.yaml      2026-07-16 
13:15:12.000000000 +0200
@@ -13,13 +13,18 @@
       - id: check-yaml
         files: .*\.(yaml|yml)$
   - repo: https://opendev.org/openstack/hacking
-    rev: 8.0.0
+    rev: 8.1.0
     hooks:
       - id: hacking
         additional_dependencies: []
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.15.5
+    rev: v0.15.19
     hooks:
       - id: ruff-check
         args: ['--fix', '--unsafe-fixes']
       - id: ruff-format
+  - repo: https://github.com/PyCQA/doc8
+    rev: v2.0.0
+    hooks:
+      - id: doc8
+        args: ['--ignore', 'D001']
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/.zuul.yaml 
new/oslo_config-10.6.0/.zuul.yaml
--- old/oslo_config-10.5.0/.zuul.yaml   2026-06-11 15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/.zuul.yaml   2026-07-16 13:15:12.000000000 +0200
@@ -1,8 +1,11 @@
+---
+
 - project:
     templates:
       - check-requirements
       - lib-forward-testing-python3
       - openstack-python3-jobs
+      - openstack-python3-next-jobs
       - periodic-stable-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/AUTHORS 
new/oslo_config-10.6.0/AUTHORS
--- old/oslo_config-10.5.0/AUTHORS      2026-06-11 15:23:46.000000000 +0200
+++ new/oslo_config-10.6.0/AUTHORS      2026-07-16 13:15:43.000000000 +0200
@@ -31,6 +31,7 @@
 Chuck Short <[email protected]>
 Chuck Short <[email protected]>
 Chung Chih, Hung <[email protected]>
+Clif Houck <[email protected]>
 Corey Bryant <[email protected]>
 Cyril Roelandt <[email protected]>
 Cyril Roelandt <[email protected]>
@@ -120,6 +121,7 @@
 Michael Still <[email protected]>
 Michał Górny <[email protected]>
 Miguel Grinberg <[email protected]>
+Mike Bayer <[email protected]>
 Mike Fedosin <[email protected]>
 Moises Guimaraes de Medeiros <[email protected]>
 Moisés Guimarães de Medeiros <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/ChangeLog 
new/oslo_config-10.6.0/ChangeLog
--- old/oslo_config-10.5.0/ChangeLog    2026-06-11 15:23:46.000000000 +0200
+++ new/oslo_config-10.6.0/ChangeLog    2026-07-16 13:15:43.000000000 +0200
@@ -1,9 +1,23 @@
 CHANGES
 =======
 
+10.6.0
+------
+
+* Name ConfigOpts serialized state version
+* ruff: Enable log-related checks
+* Support ConfigOpts state snapshots for spawn workers
+* zuul: Use openstack-python3-next-jobs template
+* pre-commit: Bump versions
+* Run doc8
+* Do not install code to build release notes
+* Loose validation for schemes
+* Better support Sphinx doc generation with custom option types
+
 10.5.0
 ------
 
+* typing: Accept Sequence to ConfigOpts.\_\_call\_\_
 * typing: Accept tuples to exception
 * Bump python version in mypy check
 * fix: setuptools.package.find is a glob
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/PKG-INFO 
new/oslo_config-10.6.0/PKG-INFO
--- old/oslo_config-10.5.0/PKG-INFO     2026-06-11 15:23:46.495775200 +0200
+++ new/oslo_config-10.6.0/PKG-INFO     2026-07-16 13:15:43.727089000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: oslo.config
-Version: 10.5.0
+Version: 10.6.0
 Summary: Oslo Configuration API
 Author-email: OpenStack <[email protected]>
 License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/doc/requirements.txt 
new/oslo_config-10.6.0/doc/requirements.txt
--- old/oslo_config-10.5.0/doc/requirements.txt 2026-06-11 15:22:55.000000000 
+0200
+++ new/oslo_config-10.6.0/doc/requirements.txt 2026-07-16 13:15:12.000000000 
+0200
@@ -3,4 +3,3 @@
 openstackdocstheme>=1.20.0 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
 fixtures>=3.0.0 # Apache-2.0/BSD
-doc8>=0.6.0 # Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo.config.egg-info/PKG-INFO 
new/oslo_config-10.6.0/oslo.config.egg-info/PKG-INFO
--- old/oslo_config-10.5.0/oslo.config.egg-info/PKG-INFO        2026-06-11 
15:23:46.000000000 +0200
+++ new/oslo_config-10.6.0/oslo.config.egg-info/PKG-INFO        2026-07-16 
13:15:43.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: oslo.config
-Version: 10.5.0
+Version: 10.6.0
 Summary: Oslo Configuration API
 Author-email: OpenStack <[email protected]>
 License-Expression: Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo.config.egg-info/SOURCES.txt 
new/oslo_config-10.6.0/oslo.config.egg-info/SOURCES.txt
--- old/oslo_config-10.5.0/oslo.config.egg-info/SOURCES.txt     2026-06-11 
15:23:46.000000000 +0200
+++ new/oslo_config-10.6.0/oslo.config.egg-info/SOURCES.txt     2026-07-16 
13:15:43.000000000 +0200
@@ -102,6 +102,7 @@
 releasenotes/notes/add_option_for_shell_completion-47b1b47d41a490e8.yaml
 releasenotes/notes/config-from-environment-3feba7b4cc747d2b.yaml
 releasenotes/notes/config-validator-256817f2183994fd.yaml
+releasenotes/notes/configopts-serialization-spawn-9f6d3a1c2b4e5f70.yaml
 releasenotes/notes/deprecate-version-module-10817e0cac220063.yaml
 releasenotes/notes/dict-key-value-separator-127ce36ba4d9245f.yaml
 releasenotes/notes/drop-python27-support-87f1b4089d4cc78b.yaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo.config.egg-info/pbr.json 
new/oslo_config-10.6.0/oslo.config.egg-info/pbr.json
--- old/oslo_config-10.5.0/oslo.config.egg-info/pbr.json        2026-06-11 
15:23:46.000000000 +0200
+++ new/oslo_config-10.6.0/oslo.config.egg-info/pbr.json        2026-07-16 
13:15:43.000000000 +0200
@@ -1 +1 @@
-{"git_version": "183a7ab", "is_release": true}
\ No newline at end of file
+{"git_version": "e148266", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/cfg.py 
new/oslo_config-10.6.0/oslo_config/cfg.py
--- old/oslo_config-10.5.0/oslo_config/cfg.py   2026-06-11 15:22:56.000000000 
+0200
+++ new/oslo_config-10.6.0/oslo_config/cfg.py   2026-07-16 13:15:12.000000000 
+0200
@@ -26,6 +26,7 @@
 import itertools
 import logging
 import os
+import pickle
 import string
 import sys
 from typing import IO, Any, Protocol, TypedDict, cast
@@ -49,6 +50,21 @@
 
 LOG = logging.getLogger(__name__)
 
+_CONFIG_OPTS_STATE_VERSION = 1
+
+
+def _identity(value: Any) -> Any:
+    return value
+
+
+def _import_config_opts_state(state: Mapping[str, Any]) -> 'ConfigOpts':
+    return ConfigOpts.import_state(state)
+
+
+def _import_global_config_opts_state(state: Mapping[str, Any]) -> 'ConfigOpts':
+    CONF.__setstate__(state)
+    return CONF
+
 
 class _SupportsLog(Protocol):
     """Protocol for objects that support the log() interface."""
@@ -210,6 +226,10 @@
         return f'Failed to parse {self.config_file}: {self.msg}'
 
 
+class ConfigOptsSerializationError(Error):
+    """Raised if a ConfigOpts instance cannot be serialized safely."""
+
+
 class ConfigSourceValueError(Error, ValueError):
     """Raised if a config source value does not match its opt type."""
 
@@ -1373,7 +1393,7 @@
     :param name: the option's name
     :param max_length: If positive integer, the value must be less than or
                        equal to this parameter.
-    :param schemes: list of valid URI schemes, e.g. 'https', 'ftp', 'git'
+    :param schemes: iterable of valid URI schemes, e.g. 'https', 'ftp', 'git'
     :param \*\*kwargs: arbitrary keyword arguments passed to :class:`Opt`
 
     .. versionadded:: 3.12
@@ -1388,7 +1408,7 @@
         self,
         name: str,
         max_length: int | None = None,
-        schemes: list[str] | None = None,
+        schemes: Iterable[str] | None = None,
         **kwargs: Any,
     ) -> None:
         type = types.URI(max_length=max_length, schemes=schemes)
@@ -1564,7 +1584,7 @@
             ConfigParser._parse_file(values, namespace)
 
     def __init__(self, name: str, **kwargs: Any) -> None:
-        super().__init__(name, cast(types.ConfigType, lambda x: x), **kwargs)
+        super().__init__(name, cast(types.ConfigType, _identity), **kwargs)
 
     def _get_argparse_kwargs(
         self, group: 'OptGroup | None', **kwargs: Any
@@ -1653,6 +1673,17 @@
     default: Any
 
 
+class _OptGroupState(TypedDict):
+    """Serialized form for an OptGroup."""
+
+    name: str
+    title: str
+    help: str | None
+    dynamic_group_owner: str
+    driver_option: str
+    opts: dict[str, _OptInfo]
+
+
 class _OptGroupGeneratorData(TypedDict):
     """Data returned by OptGroup._get_generator_data()."""
 
@@ -1786,6 +1817,75 @@
     group: OptGroup | None
 
 
+def _copy_opt_info(info: _OptInfo) -> _OptInfo:
+    copied: _OptInfo = {
+        'opt': info['opt'],
+        'cli': info['cli'],
+    }
+    if 'location' in info:
+        copied['location'] = info['location']
+    if 'override' in info:
+        copied['override'] = info['override']
+    if 'default' in info:
+        copied['default'] = info['default']
+    return copied
+
+
+def _export_group(group: OptGroup) -> _OptGroupState:
+    if group._driver_opts:
+        raise ConfigOptsSerializationError(
+            'cannot serialize ConfigOpts state because group '
+            f'{group.name!r} contains driver options. Driver option metadata '
+            'is not part of the supported runtime configuration snapshot; '
+            'avoid passing ConfigOpts instances with driver option metadata '
+            'to spawn workers or rebuild that metadata in the child process.'
+        )
+    return {
+        'name': group.name,
+        'title': group.title,
+        'help': group.help,
+        'dynamic_group_owner': group.dynamic_group_owner,
+        'driver_option': group.driver_option,
+        'opts': {
+            opt_name: _copy_opt_info(info)
+            for opt_name, info in group._opts.items()
+        },
+    }
+
+
+def _import_group(state: _OptGroupState) -> OptGroup:
+    group = OptGroup(
+        state['name'],
+        title=state['title'],
+        help=state['help'],
+        dynamic_group_owner=state['dynamic_group_owner'],
+        driver_option=state['driver_option'],
+    )
+    group._opts = state['opts']
+    return group
+
+
+def _export_namespace(namespace: '_Namespace | None') -> dict[str, Any] | None:
+    if namespace is None:
+        return None
+    return {
+        key: value
+        for key, value in namespace.__dict__.items()
+        if key != '_conf'
+    }
+
+
+def _import_namespace(
+    conf: 'ConfigOpts', state: Mapping[str, Any] | None
+) -> '_Namespace | None':
+    if state is None:
+        return None
+    namespace = _Namespace(conf)
+    namespace.__dict__.update(state)
+    namespace._conf = conf
+    return namespace
+
+
 class ParseError(iniparser.ParseError):
     def __init__(
         self, msg: str, lineno: int, line: str | None, filename: str
@@ -2291,6 +2391,14 @@
         choices=supported_shell_completion,
         help='Display a shell completion script',
     )
+    _serialized_setup_attrs = (
+        'project',
+        'prog',
+        'version',
+        'usage',
+        'default_config_files',
+        'default_config_dirs',
+    )
 
     def __init__(self) -> None:
         """Construct a ConfigOpts object."""
@@ -2320,6 +2428,190 @@
         self.register_opt(self._config_source_opt)
         self.register_cli_opt(self._shell_completion_opt)
 
+    def _export_deprecated_opts(
+        self,
+    ) -> dict[str, dict[str, dict[str, Any]]]:
+        """Return a serializable copy of ``_deprecated_opts``.
+
+        The live mapping stores raw :class:`OptGroup` references, which may
+        hold process-local argparse objects after CLI parsing. Replace those
+        references with the group name so the snapshot remains picklable; the
+        name is re-resolved to the live group on import.
+        """
+        exported: dict[str, dict[str, dict[str, Any]]] = {}
+        for group_name, entries in self._deprecated_opts.items():
+            exported[group_name] = {
+                dest: {
+                    'opt': info['opt'],
+                    'group_name': (
+                        info['group'].name
+                        if info['group'] is not None
+                        else None
+                    ),
+                }
+                for dest, info in entries.items()
+            }
+        return exported
+
+    def _import_deprecated_opts(
+        self,
+        state: Mapping[str, Mapping[str, Mapping[str, Any]]],
+    ) -> dict[str, dict[str, _DeprecatedOptInfo]]:
+        """Rebuild ``_deprecated_opts`` from an exported snapshot.
+
+        ``OptGroup`` references are re-resolved against the groups restored by
+        :meth:`__setstate__`. A valid snapshot produced by ``export_state``
+        always includes every group a deprecated entry references, because
+        ``register_opt`` creates the group before tracking it and
+        ``export_state`` serializes every group. A referenced group that is
+        absent from the restored ``_groups`` therefore indicates a corrupt or
+        hand-built state, which is rejected rather than silently repaired.
+        """
+        imported: dict[str, dict[str, _DeprecatedOptInfo]] = {}
+        for group_name, entries in state.items():
+            imported_entries: dict[str, _DeprecatedOptInfo] = {}
+            for dest, info in entries.items():
+                ref_group_name: str | None = info['group_name']
+                if ref_group_name is None:
+                    ref_group: OptGroup | None = None
+                else:
+                    ref_group = self._groups.get(ref_group_name)
+                    if ref_group is None:
+                        raise ConfigOptsSerializationError(
+                            'cannot import ConfigOpts state because the '
+                            f'deprecated option {dest!r} references group '
+                            f'{ref_group_name!r}, which is not present in '
+                            'the serialized groups. The state is invalid.'
+                        )
+                imported_entries[dest] = {
+                    'opt': info['opt'],
+                    'group': ref_group,
+                }
+            imported[group_name] = imported_entries
+        return imported
+
+    def export_state(self) -> dict[str, Any]:
+        """Return a serializable snapshot of this configuration state.
+
+        The snapshot preserves registered opts and groups, parsed CLI and
+        config-file values, defaults, overrides, and setup metadata. It
+        intentionally excludes process-local parser objects, caches, mutation
+        hooks, extension managers, and environment drivers.
+
+        Alternative configuration source objects are not serialized because
+        they may hold process-local resources. If any are loaded, exporting
+        fails instead of silently dropping meaningful configuration state.
+        """
+        if self._sources:
+            raise ConfigOptsSerializationError(
+                'cannot serialize ConfigOpts state because it contains loaded '
+                'configuration sources. Source objects may hold process-local '
+                'or non-picklable state; avoid passing this ConfigOpts '
+                'instance to spawn workers or use serializable configuration '
+                'inputs before spawning.'
+            )
+
+        state: dict[str, Any] = {
+            'version': _CONFIG_OPTS_STATE_VERSION,
+            'opts': {
+                opt_name: _copy_opt_info(info)
+                for opt_name, info in self._opts.items()
+            },
+            'groups': {
+                group_name: _export_group(group)
+                for group_name, group in self._groups.items()
+            },
+            'deprecated_opts': self._export_deprecated_opts(),
+            'args': list(self._args) if self._args is not None else None,
+            'namespace': _export_namespace(self._namespace),
+            'mutable_ns': _export_namespace(self._mutable_ns),
+            'config_opts': list(self._config_opts),
+            'cli_opts': [
+                (
+                    item['opt'],
+                    item['group'].name if item['group'] is not None else None,
+                )
+                for item in self._cli_opts
+            ],
+            'validate_default_values': self._validate_default_values,
+            'use_env': self._use_env,
+            'setup_attrs': {
+                name: getattr(self, name)
+                for name in self._serialized_setup_attrs
+                if hasattr(self, name)
+            },
+        }
+        self._assert_serializable(state)
+        return state
+
+    @classmethod
+    def import_state(cls, state: Mapping[str, Any]) -> 'ConfigOpts':
+        """Create a ConfigOpts instance from an exported state snapshot."""
+        conf = cls()
+        conf.__setstate__(state)
+        return conf
+
+    @staticmethod
+    def _assert_serializable(state: Mapping[str, Any]) -> None:
+        try:
+            pickle.dumps(state)
+        except Exception as exc:
+            raise ConfigOptsSerializationError(
+                'cannot serialize ConfigOpts state because it contains a '
+                'non-picklable option, option type, or source-related value. '
+                'Avoid passing such state to spawn workers or use '
+                f'serializable option types. Original error: {exc}'
+            ) from exc
+
+    def __getstate__(self) -> dict[str, Any]:
+        return self.export_state()
+
+    def __setstate__(self, state: Mapping[str, Any]) -> None:
+        if state.get('version') != _CONFIG_OPTS_STATE_VERSION:
+            raise ConfigOptsSerializationError(
+                'unsupported ConfigOpts serialized state version '
+                f'{state.get("version")!r}'
+            )
+
+        self._opts = state['opts']
+        self._groups = {
+            group_name: _import_group(group_state)
+            for group_name, group_state in state['groups'].items()
+        }
+        self._deprecated_opts = self._import_deprecated_opts(
+            state['deprecated_opts']
+        )
+        self._args = state['args']
+        self._oparser = None
+        self._namespace = _import_namespace(self, state['namespace'])
+        self._mutable_ns = _import_namespace(self, state['mutable_ns'])
+        self._mutate_hooks = set()
+        self.__cache = {}
+        self.__drivers_cache = {}
+        self._config_opts = state['config_opts']
+        self._cli_opts = collections.deque(
+            {
+                'opt': opt,
+                'group': self._groups[group_name]
+                if group_name is not None
+                else None,
+            }
+            for opt, group_name in state['cli_opts']
+        )
+        self._validate_default_values = state['validate_default_values']
+        self._sources = []
+        self._ext_mgr = None
+        self._use_env = state['use_env']
+        self._env_driver = _environment.EnvironmentConfigurationSource()
+
+        for name, value in state['setup_attrs'].items():
+            setattr(self, name, value)
+
+    def __reduce__(self) -> tuple[Any, tuple[dict[str, Any]]]:
+        if self is CONF:
+            return (_import_global_config_opts_state, (self.export_state(),))
+        return (_import_config_opts_state, (self.export_state(),))
+
     def _pre_setup(
         self,
         project: str | None,
@@ -2449,7 +2741,7 @@
 
     def __call__(
         self,
-        args: list[str] | None = None,
+        args: Sequence[str] | None = None,
         project: str | None = None,
         prog: str | None = None,
         version: str | None = None,
@@ -3618,7 +3910,7 @@
                 if self._get(opt.dest, group, namespace) is None:
                     raise RequiredOptError(opt.name, group)
 
-    def _parse_cli_opts(self, args: list[str]) -> '_Namespace':
+    def _parse_cli_opts(self, args: Sequence[str]) -> '_Namespace':
         """Parse command line options.
 
         Initializes the command line option parser and parses the supplied
@@ -3630,7 +3922,7 @@
                  ConfigFileParseError, ConfigFileValueError
 
         """
-        self._args = args
+        self._args = list(args)
         assert self._oparser is not None
         for opt, group in self._all_cli_opts():
             opt._add_to_cli(self._oparser, group)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/sphinxconfiggen.py 
new/oslo_config-10.6.0/oslo_config/sphinxconfiggen.py
--- old/oslo_config-10.5.0/oslo_config/sphinxconfiggen.py       2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/oslo_config/sphinxconfiggen.py       2026-07-16 
13:15:12.000000000 +0200
@@ -62,7 +62,7 @@
     app: Sphinx, config_file: str, base_name: str | None
 ) -> None:
     def info(msg: str) -> None:
-        LOG.info(f'[{__name__}] {msg}')
+        LOG.info('[%s] {%s}', __name__, msg)
 
     # If we are given a file that isn't an absolute path, look for it
     # in the source directory if it doesn't exist.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/sphinxext.py 
new/oslo_config-10.6.0/oslo_config/sphinxext.py
--- old/oslo_config-10.5.0/oslo_config/sphinxext.py     2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/oslo_config/sphinxext.py     2026-07-16 
13:15:12.000000000 +0200
@@ -93,6 +93,9 @@
 }
 
 
+_UNKNOWN_TYPE = 'unknown type'
+
+
 def _get_choice_text(choice: Any) -> str:
     if choice is None:
         return '<None>'
@@ -102,7 +105,13 @@
 
 
 def _format_opt(opt: Any, group_name: str) -> Generator[str, None, None]:
-    opt_type = _TYPE_DESCRIPTIONS.get(type(opt), 'unknown type')
+    opt_type = _TYPE_DESCRIPTIONS.get(type(opt), _UNKNOWN_TYPE)
+
+    # NOTE(clif): If we don't have a type description handy, fall back to
+    # trying to retrieve the type from opt.type if available.
+    if opt_type == _UNKNOWN_TYPE and hasattr(opt, 'type'):
+        opt_type = getattr(opt.type, 'type_name', _UNKNOWN_TYPE)
+
     yield f'.. oslo.config:option:: {opt.dest}'
     yield ''
     yield _indent(f':Type: {opt_type}')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/tests/test_cfg.py 
new/oslo_config-10.6.0/oslo_config/tests/test_cfg.py
--- old/oslo_config-10.5.0/oslo_config/tests/test_cfg.py        2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/oslo_config/tests/test_cfg.py        2026-07-16 
13:15:12.000000000 +0200
@@ -17,11 +17,14 @@
 import functools
 import io
 import logging
+import multiprocessing
 import os
+import pickle
+import queue
 import shutil
 import sys
 import tempfile
-from typing import Any
+from typing import Any, cast
 import unittest
 from unittest import mock
 
@@ -35,6 +38,46 @@
 load_tests = testscenarios.load_tests_apply_scenarios
 
 
+class _UnserializableType:
+    def __init__(self):
+        self._not_picklable = lambda value: value
+
+    def __call__(self, value):
+        return value
+
+
+def _spawn_config_opts_worker(conf, result_queue):
+    try:
+        assert conf.spawn_default == 'changed-default'
+        assert conf.spawn_override == 'override-value'
+        assert conf.spawn_cli == 'cli-value'
+        assert conf.spawn_file == 'file-value'
+        assert conf.spawn_bool is True
+        assert conf.spawn_int == 42
+        assert conf.spawn_list == ['alpha', 'beta']
+        assert conf.spawn_dict == {'one': '1', 'two': '2'}
+        assert conf.spawn_multi == ['first', 'second']
+        assert conf.spawn_group.spawn_group_default == 'group-default'
+        assert conf.spawn_group.spawn_group_override == 'group-override'
+        assert conf.spawn_group.spawn_group_file == 'group-file'
+    except Exception as exc:
+        result_queue.put(('error', repr(exc)))
+    else:
+        result_queue.put(('ok', None))
+
+
+def _spawn_global_conf_worker(conf, result_queue):
+    try:
+        assert conf is cfg.CONF
+        assert cfg.CONF.ovn.ovsdb_retry_max_interval == 42
+        assert cfg.CONF.ovn.ovn_metadata_enabled is True
+        assert cfg.CONF.agent.extensions == ['metadata']
+    except Exception as exc:
+        result_queue.put(('error', repr(exc)))
+    else:
+        result_queue.put(('ok', None))
+
+
 class ExceptionsTestCase(base.BaseTestCase):
     def test_error(self):
         msg = str(cfg.Error('foobar'))
@@ -5583,6 +5626,448 @@
         self.assertRaises(cfg.NotInitializedError, conf.print_help)
 
 
+class ConfigOptsSerializationTestCase(BaseTestCase):
+    def _roundtrip_pickle(self, conf):
+        return pickle.loads(pickle.dumps(conf))
+
+    def test_basic_export_import_state(self):
+        conf = cfg.ConfigOpts()
+        conf([])
+
+        restored = cfg.ConfigOpts.import_state(conf.export_state())
+
+        self.assertEqual([], restored.config_file)
+        self.assertEqual([], restored.config_dir)
+
+    def test_basic_pickle_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf([])
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual([], restored.config_file)
+        self.assertEqual([], restored.config_dir)
+
+    def test_unknown_state_version_fails_clearly(self):
+        conf = cfg.ConfigOpts()
+        state = conf.export_state()
+        state['version'] = 2
+
+        exc = self.assertRaises(
+            cfg.ConfigOptsSerializationError,
+            cfg.ConfigOpts.import_state,
+            state,
+        )
+        self.assertIn(
+            'unsupported ConfigOpts serialized state version', str(exc)
+        )
+
+    def test_registered_options_survive_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('registered_opt', default='registered'))
+        conf([])
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('registered', restored.registered_opt)
+
+    def test_common_option_types_survive_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_cli_opt(cfg.BoolOpt('bool_opt'))
+        conf.register_cli_opt(cfg.IntOpt('int_opt'))
+        conf.register_cli_opt(cfg.ListOpt('list_opt'))
+        conf.register_cli_opt(cfg.DictOpt('dict_opt'))
+        conf.register_cli_opt(cfg.MultiStrOpt('multi_opt'))
+        conf(
+            [
+                '--bool_opt',
+                '--int_opt=42',
+                '--list_opt=alpha,beta',
+                '--dict_opt=one:1,two:2',
+                '--multi_opt=first',
+                '--multi_opt=second',
+            ]
+        )
+
+        restored_from_state = cfg.ConfigOpts.import_state(conf.export_state())
+        restored_from_pickle = self._roundtrip_pickle(conf)
+
+        for restored in (restored_from_state, restored_from_pickle):
+            self.assertIs(True, restored.bool_opt)
+            self.assertEqual(42, restored.int_opt)
+            self.assertEqual(['alpha', 'beta'], restored.list_opt)
+            self.assertEqual({'one': '1', 'two': '2'}, restored.dict_opt)
+            self.assertEqual(['first', 'second'], restored.multi_opt)
+
+    def test_set_default_before_parse_survives_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('default_opt', default='original'))
+        conf.set_default('default_opt', 'changed')
+        conf([])
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('changed', restored.default_opt)
+
+    def test_set_default_after_parse_survives_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('default_opt', default='original'))
+        conf([])
+        conf.set_default('default_opt', 'changed')
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('changed', restored.default_opt)
+
+    def test_set_override_before_parse_survives_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('override_opt', default='original'))
+        conf.set_override('override_opt', 'override')
+        conf([])
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('override', restored.override_opt)
+
+    def test_set_override_after_parse_survives_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('override_opt', default='original'))
+        conf([])
+        conf.set_override('override_opt', 'override')
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('override', restored.override_opt)
+
+    def test_group_values_survive_serialization(self):
+        conf = cfg.ConfigOpts()
+        conf.register_group(cfg.OptGroup('serial_group'))
+        conf.register_opt(
+            cfg.StrOpt('group_default_opt', default='original'),
+            group='serial_group',
+        )
+        conf.register_opt(
+            cfg.StrOpt('group_override_opt', default='original'),
+            group='serial_group',
+        )
+        conf.set_default('group_default_opt', 'changed', group='serial_group')
+        conf.set_override(
+            'group_override_opt', 'override', group='serial_group'
+        )
+        conf([])
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('changed', restored.serial_group.group_default_opt)
+        self.assertEqual('override', restored.serial_group.group_override_opt)
+
+    def test_config_file_values_survive_serialization(self):
+        paths = self.create_tempfiles(
+            [('serialization', '[DEFAULT]\nfile_opt = file-value\n')]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('file_opt'))
+        conf(args=[], default_config_files=paths, use_env=False)
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('file-value', restored.file_opt)
+        self.assertEqual(paths, restored.config_file)
+
+    def test_multiple_config_files_preserve_precedence(self):
+        paths = self.create_tempfiles(
+            [
+                ('serialization-1', '[DEFAULT]\nfile_opt = first\n'),
+                ('serialization-2', '[DEFAULT]\nfile_opt = second\n'),
+            ]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('file_opt'))
+        conf(args=[], default_config_files=paths, use_env=False)
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('second', restored.file_opt)
+        self.assertEqual(paths, restored.config_file)
+
+    def test_group_values_from_multiple_config_files_preserve_precedence(self):
+        paths = self.create_tempfiles(
+            [
+                (
+                    'serialization-group-1',
+                    '[serial_group]\nfile_opt = first\n',
+                ),
+                (
+                    'serialization-group-2',
+                    '[serial_group]\nfile_opt = second\n',
+                ),
+            ]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_group(cfg.OptGroup('serial_group'))
+        conf.register_opt(cfg.StrOpt('file_opt'), group='serial_group')
+        conf(args=[], default_config_files=paths, use_env=False)
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('second', restored.serial_group.file_opt)
+
+    def test_deprecated_opts_survive_serialization(self):
+        paths = self.create_tempfiles(
+            [('deprecated-serialization', '[DEFAULT]\nold_opt = from-file\n')]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_opt(
+            cfg.StrOpt(
+                'new_opt',
+                deprecated_opts=[cfg.DeprecatedOpt('old_opt')],
+            )
+        )
+        conf(args=[], default_config_files=paths, use_env=False)
+
+        restored = self._roundtrip_pickle(conf)
+
+        self.assertEqual('from-file', restored.new_opt)
+
+    def test_deprecated_opts_in_group_with_cli_opt_survive_serialization(self):
+        paths = self.create_tempfiles(
+            [
+                (
+                    'deprecated-group-cli',
+                    '[serial_group]\nold_group_opt = group-from-file\n',
+                )
+            ]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_group(cfg.OptGroup('serial_group'))
+        # A CLI opt in the named group forces OptGroup._argparse_group to be
+        # populated with an unpicklable argparse object during parsing.
+        conf.register_cli_opt(
+            cfg.StrOpt('serial_group_cli'), group='serial_group'
+        )
+        conf.register_opt(
+            cfg.StrOpt(
+                'new_group_opt',
+                deprecated_opts=[
+                    cfg.DeprecatedOpt('old_group_opt', group='serial_group')
+                ],
+            ),
+            group='serial_group',
+        )
+        conf(args=[], default_config_files=paths, use_env=False)
+
+        restored = self._roundtrip_pickle(conf)
+
+        # The deprecated name in the config file still resolves to the new opt.
+        self.assertEqual(
+            'group-from-file', restored.serial_group.new_group_opt
+        )
+        # The deprecated-alias reverse lookup survives and is keyed on the
+        # group.
+        self.assertEqual(
+            ('new_group_opt', 'serial_group'),
+            restored._find_deprecated_opts('old_group_opt', 'serial_group'),
+        )
+        # The restored _deprecated_opts re-resolves to the live group instance
+        # rather than carrying a duplicate OptGroup with process-local state.
+        dep_group = restored._deprecated_opts['serial_group']['old_group_opt'][
+            'group'
+        ]
+        self.assertIs(dep_group, restored._groups['serial_group'])
+        self.assertIsNone(dep_group._argparse_group)
+
+    def test_import_state_rejects_missing_referenced_group(self):
+        # A valid state produced by export_state always includes every group a
+        # deprecated entry references. A state where such a group is absent is
+        # corrupt; import should reject it rather than silently repairing it.
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('new_opt'))
+        conf([])
+        state = conf.export_state()
+        state['deprecated_opts']['ghost_group'] = {
+            'old_x': {
+                'opt': conf._opts['new_opt']['opt'],
+                'group_name': 'ghost_group',
+            }
+        }
+
+        exc = self.assertRaises(
+            cfg.ConfigOptsSerializationError,
+            cfg.ConfigOpts.import_state,
+            state,
+        )
+        self.assertIn(
+            "references group 'ghost_group', which is not present",
+            str(exc),
+        )
+
+    def test_repeated_pickle_roundtrip(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('roundtrip_opt', default='original'))
+        conf.set_override('roundtrip_opt', 'override')
+        conf([])
+
+        restored = self._roundtrip_pickle(self._roundtrip_pickle(conf))
+
+        self.assertEqual('override', restored.roundtrip_opt)
+
+    def test_unserializable_opt_fails_clearly(self):
+        conf = cfg.ConfigOpts()
+        conf.register_opt(
+            cfg.Opt(
+                'broken_opt',
+                type=cast(types.ConfigType, _UnserializableType()),
+            )
+        )
+        conf([])
+
+        exc = self.assertRaises(
+            cfg.ConfigOptsSerializationError,
+            conf.export_state,
+        )
+        self.assertIn('cannot serialize ConfigOpts state', str(exc))
+
+    def test_loaded_sources_fail_clearly(self):
+        conf = cfg.ConfigOpts()
+        conf._sources.append(mock.Mock())
+
+        exc = self.assertRaises(
+            cfg.ConfigOptsSerializationError,
+            conf.export_state,
+        )
+        self.assertIn('loaded configuration sources', str(exc))
+
+    def test_driver_opts_fail_clearly(self):
+        conf = cfg.ConfigOpts()
+        group = cfg.OptGroup('driver_group')
+        group._save_driver_opts({'driver': [cfg.StrOpt('driver_opt')]})
+        conf.register_group(group)
+
+        exc = self.assertRaises(
+            cfg.ConfigOptsSerializationError,
+            conf.export_state,
+        )
+        self.assertIn('contains driver options', str(exc))
+
+    def test_multiprocessing_spawn_observes_parent_state(self):
+        paths = self.create_tempfiles(
+            [
+                (
+                    'spawn',
+                    '[DEFAULT]\n'
+                    'spawn_file = file-value\n'
+                    'spawn_bool = true\n'
+                    'spawn_int = 42\n'
+                    'spawn_list = alpha,beta\n'
+                    'spawn_dict = one:1,two:2\n'
+                    'spawn_multi = first\n'
+                    'spawn_multi = second\n'
+                    '[spawn_group]\n'
+                    'spawn_group_file = group-file\n',
+                )
+            ]
+        )
+        conf = cfg.ConfigOpts()
+        conf.register_opt(cfg.StrOpt('spawn_default', default='original'))
+        conf.register_opt(cfg.StrOpt('spawn_override', default='original'))
+        conf.register_cli_opt(cfg.StrOpt('spawn_cli'))
+        conf.register_opt(cfg.StrOpt('spawn_file'))
+        conf.register_opt(cfg.BoolOpt('spawn_bool'))
+        conf.register_opt(cfg.IntOpt('spawn_int'))
+        conf.register_opt(cfg.ListOpt('spawn_list'))
+        conf.register_opt(cfg.DictOpt('spawn_dict'))
+        conf.register_opt(cfg.MultiStrOpt('spawn_multi'))
+        conf.register_group(cfg.OptGroup('spawn_group'))
+        conf.register_opt(
+            cfg.StrOpt('spawn_group_default', default='group-original'),
+            group='spawn_group',
+        )
+        conf.register_opt(
+            cfg.StrOpt('spawn_group_override', default='group-original'),
+            group='spawn_group',
+        )
+        conf.register_opt(
+            cfg.StrOpt('spawn_group_file'),
+            group='spawn_group',
+        )
+        conf(
+            args=['--spawn_cli=cli-value'],
+            default_config_files=paths,
+            use_env=False,
+        )
+        conf.set_default('spawn_default', 'changed-default')
+        conf.set_override('spawn_override', 'override-value')
+        conf.set_default(
+            'spawn_group_default', 'group-default', group='spawn_group'
+        )
+        conf.set_override(
+            'spawn_group_override', 'group-override', group='spawn_group'
+        )
+
+        ctx = multiprocessing.get_context('spawn')
+        result_queue = ctx.Queue()
+        proc = ctx.Process(
+            target=_spawn_config_opts_worker,
+            args=(conf, result_queue),
+        )
+        proc.start()
+        proc.join(10)
+        if proc.is_alive():
+            proc.terminate()
+            proc.join()
+            self.fail('spawn worker did not finish')
+
+        try:
+            status, details = result_queue.get(timeout=5)
+        except queue.Empty:
+            self.fail('spawn worker did not report a result')
+
+        self.assertEqual(0, proc.exitcode)
+        self.assertEqual(('ok', None), (status, details))
+
+    def test_multiprocessing_spawn_restores_global_conf_singleton(self):
+        state = cfg.CONF.export_state()
+        self.addCleanup(cfg.CONF.__setstate__, state)
+        cfg.CONF.__setstate__(cfg.ConfigOpts().export_state())
+
+        cfg.CONF.register_group(cfg.OptGroup('ovn'))
+        cfg.CONF.register_opt(
+            cfg.IntOpt('ovsdb_retry_max_interval', default=10),
+            group='ovn',
+        )
+        cfg.CONF.register_opt(
+            cfg.BoolOpt('ovn_metadata_enabled', default=False),
+            group='ovn',
+        )
+        cfg.CONF.register_group(cfg.OptGroup('agent'))
+        cfg.CONF.register_opt(cfg.ListOpt('extensions'), group='agent')
+        cfg.CONF([])
+        cfg.CONF.set_override('ovsdb_retry_max_interval', 42, group='ovn')
+        cfg.CONF.set_override('ovn_metadata_enabled', True, group='ovn')
+        cfg.CONF.set_override('extensions', ['metadata'], group='agent')
+
+        ctx = multiprocessing.get_context('spawn')
+        result_queue = ctx.Queue()
+        proc = ctx.Process(
+            target=_spawn_global_conf_worker,
+            args=(cfg.CONF, result_queue),
+        )
+        proc.start()
+        proc.join(10)
+        if proc.is_alive():
+            proc.terminate()
+            proc.join()
+            self.fail('spawn worker did not finish')
+
+        try:
+            status, details = result_queue.get(timeout=5)
+        except queue.Empty:
+            self.fail('spawn worker did not report a result')
+
+        self.assertEqual(0, proc.exitcode)
+        self.assertEqual(('ok', None), (status, details))
+
+
 class OptTestCase(base.BaseTestCase):
     def test_opt_eq(self):
         d1 = cfg.ListOpt('oldfoo')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/oslo_config-10.5.0/oslo_config/tests/test_sphinxext.py 
new/oslo_config-10.6.0/oslo_config/tests/test_sphinxext.py
--- old/oslo_config-10.5.0/oslo_config/tests/test_sphinxext.py  2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/oslo_config/tests/test_sphinxext.py  2026-07-16 
13:15:12.000000000 +0200
@@ -17,6 +17,7 @@
 
 from oslo_config import cfg
 from oslo_config import sphinxext
+from oslo_config import types
 
 
 class FormatGroupTest(base.BaseTestCase):
@@ -81,6 +82,49 @@
         ''').lstrip(),
             results,
         )
+
+    def test_with_custom_type(self):
+        TEST_TYPE_NAME = 'test type'
+
+        class TestType(types.ConfigType):
+            def __init__(self):
+                super().__init__(type_name=TEST_TYPE_NAME)
+
+            def __call__(self, value):
+                return str(value)
+
+            def _formatter(self, value):
+                return self.quote_trailing_and_leading_space(value)
+
+        results = '\n'.join(
+            list(
+                sphinxext._format_group_opts(
+                    namespace=None,
+                    group_name=None,
+                    group_obj=None,
+                    opt_list=[
+                        cfg.Opt(
+                            'custom_type_opt_name',
+                            type=TestType(),
+                            help='this is a custom option type',
+                        ),
+                    ],
+                )
+            )
+        )
+        self.assertEqual(
+            textwrap.dedent('''
+        .. oslo.config:group:: DEFAULT
+
+        .. oslo.config:option:: custom_type_opt_name
+
+          :Type: test type
+          :Default: ``<None>``
+
+          this is a custom option type
+        ''').lstrip(),
+            results,
+        )
 
     def test_with_min(self):
         results = '\n'.join(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/types.py 
new/oslo_config-10.6.0/oslo_config/types.py
--- old/oslo_config-10.5.0/oslo_config/types.py 2026-06-11 15:22:55.000000000 
+0200
+++ new/oslo_config-10.6.0/oslo_config/types.py 2026-07-16 13:15:12.000000000 
+0200
@@ -21,7 +21,7 @@
 """
 
 import collections
-from collections.abc import Callable, KeysView
+from collections.abc import Callable, Iterable, KeysView
 import operator
 import re
 import warnings
@@ -1056,7 +1056,7 @@
     def __init__(
         self,
         max_length: int | None = None,
-        schemes: list[str] | None = None,
+        schemes: Iterable[str] | None = None,
         type_name: str = 'uri value',
     ) -> None:
         super().__init__(type_name=type_name)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/oslo_config/validator.py 
new/oslo_config-10.6.0/oslo_config/validator.py
--- old/oslo_config-10.5.0/oslo_config/validator.py     2026-06-11 
15:22:55.000000000 +0200
+++ new/oslo_config-10.6.0/oslo_config/validator.py     2026-07-16 
13:15:12.000000000 +0200
@@ -83,6 +83,9 @@
 ]
 
 
+LOG = logging.getLogger(__name__)
+
+
 KNOWN_BAD_GROUPS = ['keystone_authtoken']
 
 
@@ -127,7 +130,7 @@
         if group in conf.exclude_group:
             continue
         if group not in sections:
-            logging.warning(
+            LOG.warning(
                 'Group %s from the sample config is not defined in input-file',
                 group,
             )
@@ -156,7 +159,7 @@
                 keyname = opt.get('dest')
 
             if any(rex.fullmatch(keyname) for rex in exclusion_regexes):
-                logging.info(
+                LOG.info(
                     '%s/%s Ignoring option because it is part of the excluded '
                     'patterns. This can be changed with the --exclude-options '
                     'argument',
@@ -166,11 +169,9 @@
                 continue
 
             if len(value) > 1:
-                logging.info(
-                    '%s/%s defined %s times', group, keyname, len(value)
-                )
+                LOG.info('%s/%s defined %s times', group, keyname, len(value))
             if not opt['default']:
-                logging.warning(
+                LOG.warning(
                     '%s/%s sample value is empty but input-file has %s',
                     group,
                     keyname,
@@ -178,7 +179,7 @@
                 )
                 warnings = True
             elif not frozenset(defaults).intersection(value):
-                logging.warning(
+                LOG.warning(
                     '%s/%s sample value %s is not in %s',
                     group,
                     keyname,
@@ -224,11 +225,11 @@
             continue
         for option in options:
             if _validate_deprecated_opt(section, option, opt_data):
-                logging.warning('Deprecated opt %s/%s found', section, option)
+                LOG.warning('Deprecated opt %s/%s found', section, option)
                 warnings = True
             elif not _validate_opt(section, option, opt_data):
                 if section in KNOWN_BAD_GROUPS:
-                    logging.info(
+                    LOG.info(
                         'Ignoring missing option "%s" from group '
                         '"%s" because the group is known to have '
                         'incomplete sample config data and thus '
@@ -237,7 +238,7 @@
                         section,
                     )
                     continue
-                logging.error(
+                LOG.error(
                     '%s/%s is not part of the sample config', section, option
                 )
                 errors = True
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/pyproject.toml 
new/oslo_config-10.6.0/pyproject.toml
--- old/oslo_config-10.5.0/pyproject.toml       2026-06-11 15:22:55.000000000 
+0200
+++ new/oslo_config-10.6.0/pyproject.toml       2026-07-16 13:15:12.000000000 
+0200
@@ -75,7 +75,7 @@
 docstring-code-format = true
 
 [tool.ruff.lint]
-select = ["E4", "E5", "E7", "E9", "F", "S", "UP", "W"]
+select = ["E4", "E5", "E7", "E9", "F", "S", "G", "LOG", "UP", "W"]
 external = ["H"]
 ignore = [
     "S101",  # asserts are only used for type narrowing
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/oslo_config-10.5.0/releasenotes/notes/configopts-serialization-spawn-9f6d3a1c2b4e5f70.yaml
 
new/oslo_config-10.6.0/releasenotes/notes/configopts-serialization-spawn-9f6d3a1c2b4e5f70.yaml
--- 
old/oslo_config-10.5.0/releasenotes/notes/configopts-serialization-spawn-9f6d3a1c2b4e5f70.yaml
      1970-01-01 01:00:00.000000000 +0100
+++ 
new/oslo_config-10.6.0/releasenotes/notes/configopts-serialization-spawn-9f6d3a1c2b4e5f70.yaml
      2026-07-16 13:15:12.000000000 +0200
@@ -0,0 +1,14 @@
+---
+features:
+  - |
+    ``ConfigOpts`` instances can now export and import a serialized state and
+    use that state for pickle serialization. This allows configuration state to
+    be passed to multiprocessing workers that use the ``spawn`` start method.
+    The serialized state preserves registered options and groups, parsed
+    command-line and config-file values, defaults, overrides, and setup
+    metadata.
+
+    Process-local helpers such as argparse parsers, caches, extension managers,
+    mutation hooks, and environment drivers are intentionally excluded from the
+    serialized state. Loaded configuration source objects are rejected with
+    ``ConfigOptsSerializationError`` instead of being silently dropped.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/oslo_config-10.5.0/tox.ini 
new/oslo_config-10.6.0/tox.ini
--- old/oslo_config-10.5.0/tox.ini      2026-06-11 15:22:56.000000000 +0200
+++ new/oslo_config-10.6.0/tox.ini      2026-07-16 13:15:12.000000000 +0200
@@ -13,7 +13,6 @@
   
{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
 deps =
   -r{toxinidir}/test-requirements.txt
-  -r{toxinidir}/requirements.txt
   -r{toxinidir}/doc/requirements.txt
 commands =
   find . -type f -name "*.pyc" -delete
@@ -64,6 +63,7 @@
   sphinx-build -W --keep-going -b html -j auto doc/source doc/build/html 
{posargs}
 
 [testenv:releasenotes]
+skip_install = true
 allowlist_externals =
   rm
 deps = {[testenv:docs]deps}

Reply via email to