[neon/forks/sip6/Neon/release] /: New upstream version 6.15.3

Dmitry Shachnev null at kde.org
Mon Jul 27 12:04:48 BST 2026


Git commit 4d53982dabb35a95bfeb43b98179c4ccdc0c7009 by Dmitry Shachnev.
Committed on 24/03/2026 at 12:56.
Pushed by carlosdem into branch 'Neon/release'.

New upstream version 6.15.3

M  +3    -3    .git_archival.txt
M  +1    -1    docs/conf.py
M  +14   -7    docs/directives.rst
M  +12   -3    docs/examples.rst
M  +22   -0    docs/releases.md
M  +1    -0    examples/package/core/core.sip
M  +2    -0    examples/standalone/fib.sip
M  +6    -8    sipbuild/generator/outputs/code/backends/abstract_backend.py
M  +77   -58   sipbuild/generator/outputs/code/backends/v12v13.py
M  +23   -65   sipbuild/generator/outputs/code/snippets.py
M  +34   -0    sipbuild/generator/outputs/code/utils.py
M  +14   -6    sipbuild/generator/parser/parser_manager.py
M  +10   -5    sipbuild/generator/resolver/resolver.py
M  +7    -1    sipbuild/generator/utils.py
M  +12   -1    test/conftest.py
A  +26   -0    test/extenders/core_module.sip
A  +35   -0    test/extenders/extras1_module.sip
A  +28   -0    test/extenders/extras2_module.sip
A  +39   -0    test/extenders/test_extenders.py
M  +1    -1    test/imported_exceptions/handler_module.sip
M  +0    -17   test/imports/core_module.sip
M  +0    -22   test/imports/extras_module.sip
M  +0    -7    test/imports/test_imports.py
M  +6    -6    test/movable/movable_module.sip
M  +8    -8    test/template_typedef/template_typedef_module.sip

https://invent.kde.org/neon/forks/sip6/-/commit/4d53982dabb35a95bfeb43b98179c4ccdc0c7009

diff --git a/.git_archival.txt b/.git_archival.txt
index 0bbb749..2b23ab6 100644
--- a/.git_archival.txt
+++ b/.git_archival.txt
@@ -1,3 +1,3 @@
-node: cfbd43a5641b6e0f5c51ae7b51d79449db394127
-node-date: 2026-03-09T11:57:57Z
-describe-name: 6.15.2
+node: b7a14f4ae72d5eab4e4823dbfa2fb63e56910348
+node-date: 2026-03-24T11:51:06Z
+describe-name: 6.15.3
diff --git a/docs/conf.py b/docs/conf.py
index 4f184f5..728dd0d 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,7 +13,7 @@ project = 'sip'
 copyright = '{0} Phil Thompson <phil at riverbankcomputing.com>'.format(
         date.today().year)
 author = 'Phil Thompson'
-version = 'v6.15.2'
+version = 'v6.15.3'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/docs/directives.rst b/docs/directives.rst
index 13390aa..d3c61bb 100644
--- a/docs/directives.rst
+++ b/docs/directives.rst
@@ -1724,15 +1724,22 @@ then the pattern should instead be::
 
 .. versionadded:: 6.10
 
-This directive is used to specify the minimum version of the ABI required to
-build the project.  It normally reflects the calls made to the public API from
-handwritten code, ie. if the code make a call to a function that was added to a
-particular ABI version then that version should be specified as the minimum
-required.
+This directive is used to specify the exact major version number and the
+minimum minor version number of the ABI required to build the project.  It
+normally reflects the calls made to the public API from handwritten code, ie.
+if the code makes a call to a function that was added to a particular ABI
+version then that version should be specified as the minimum required.
+
+If the directive is also specified in an imported module (see
+:directive:`%Import`), then SIP will check that the versions do not conflict.
+
+If the major version is omitted (and not provide by an imported module) then
+the default ABI will be used.  The default may change in the future and so this
+directive should always be used in this module or an imported one.
 
 If the minor version is omitted then the latest minor version of the major
-version will be used.  However this isn't particularly useful and it is
-recommended that a minor number is always specified, even if it is ``0``.
+version will be used.  However it is recommended that a minor number is always
+specified, even if it is ``0``.
 
 For example::
 
diff --git a/docs/examples.rst b/docs/examples.rst
index fb2b067..322b65f 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -51,8 +51,12 @@ standalone projects this would normally be the same as the name defined in the
 :file:`pyproject.toml` file.  It also specifies that the code being wrapped is
 implemented in C (as opposed to C++).
 
-The next line of interest is the declaration of the :c:func:`fib_n` function to
-be wrapped.
+The next line of interest is the :directive:`MinimumABIVersion` directive.
+This specifies the ABI version being targeted.  This example is so simple that
+it will will work with any ABI version but it is good practice to specify a
+particular version.
+
+Next is the declaration of the :c:func:`fib_n` function to be wrapped.
 
 The remainder of the file is the :directive:`%MethodCode` directive attached to
 the function declaration.  This is used to provide the actual implementation of
@@ -304,6 +308,9 @@ The :directive:`%Module` directive, as well as specifying the full package name
 of the :mod:`~examples.core` module, specifies that the bindings will use the
 `PEP 384 <https://www.python.org/dev/peps/pep-0384/>`__ stable ABI.
 
+The :directive:`%MinimumABIVersion` directive specifies the ABI version being
+targeted.
+
 The :directive:`%DefaultEncoding` directive specifies that any character
 conversions between C/C++ and Python ``str`` objects will default to the ASCII
 codec.
@@ -370,6 +377,8 @@ We next look at the :file:`extras.sip` file (downloadable from
 This is very similar to the :mod:`~examples.core` module in that it implements
 simple platform-specific functions.  The key thing to notice is that there is
 no need to specify the platform tag as part of the configuration as it is
-obtained automatically from the installed ``examples-core`` project.
+obtained automatically from the installed ``examples-core`` project.  Also note
+that the target ABI version has not been specified and the version will be
+taken from the ``examples-core`` project.
 
 The ``examples-extras`` project has no need for a :file:`project.py` file.
diff --git a/docs/releases.md b/docs/releases.md
index 5190958..f27f2ce 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1,5 +1,27 @@
 # Release Notes
 
+## v6.15.3
+
+### Bug fixes
+
+- Fixed the handling of virtuals that return enums with a non-`int` base type.
+  Resolves [#105](https://github.com/Python-SIP/sip/issues/105)
+- A regression in v6.15.2 that caused a crash if `MinimumABIVersion` was not
+  specified was fixed.  Resolves
+  [#106](https://github.com/Python-SIP/sip/issues/106)
+
+### Deprecations
+
+The use of the `%MinimumABIVersion` directive will be required in SIP v7.
+Failing to use it now results in a deprecation warning.
+
+### Testing
+
+- The tests now amend `CXXFLAGS` instead of overwriting them.  This fixes
+  `blhc` (build log hardening check) in Debian.  Pull request
+  [#104](https://github.com/Python-SIP/sip/pull/104).
+
+
 ## v6.15.2
 
 ### Enhancements
diff --git a/examples/package/core/core.sip b/examples/package/core/core.sip
index 5e7073c..a16c9a5 100644
--- a/examples/package/core/core.sip
+++ b/examples/package/core/core.sip
@@ -2,6 +2,7 @@
 
 %Module(name=examples.core, use_limited_api=True)
 
+%MinimumABIVersion "13.1"
 %DefaultEncoding "ASCII"
 
 %Platforms {Linux macOS Windows}
diff --git a/examples/standalone/fib.sip b/examples/standalone/fib.sip
index 63a8da6..16cedc9 100644
--- a/examples/standalone/fib.sip
+++ b/examples/standalone/fib.sip
@@ -2,6 +2,8 @@
 
 %Module(name=fib, language="C")
 
+%MinimumABIVersion "13.1"
+
 int fib_n(int n);
 %MethodCode
     if (a0 <= 0)
diff --git a/sipbuild/generator/outputs/code/backends/abstract_backend.py b/sipbuild/generator/outputs/code/backends/abstract_backend.py
index 2219758..9ce45c1 100644
--- a/sipbuild/generator/outputs/code/backends/abstract_backend.py
+++ b/sipbuild/generator/outputs/code/backends/abstract_backend.py
@@ -44,6 +44,12 @@ class AbstractBackend(ABC):
 
         ...
 
+    @abstractmethod
+    def g_class_spec_extern_decl(self, sf, klass):
+        """ Generate the extern declaration of a class specification. """
+
+        ...
+
     @abstractmethod
     def g_conversion_to_enum(self, sf, enum):
         """ Generate the code to convert a Python enum (sipSelf) to a C/C++
@@ -197,14 +203,6 @@ class AbstractBackend(ABC):
         # This default implementation does nothing.
         pass
 
-    @abstractmethod
-    def g_py_method_table(self, sf, bindings, members, scope):
-        """ Generate a Python method table for a class or mapped type and
-        return the number of entries.
-        """
-
-        ...
-
     @abstractmethod
     def g_sip_api(self, sf, module_name, module_closure):
         """ Generate the SIP API as seen by generated code. """
diff --git a/sipbuild/generator/outputs/code/backends/v12v13.py b/sipbuild/generator/outputs/code/backends/v12v13.py
index eb54fcf..03b458a 100644
--- a/sipbuild/generator/outputs/code/backends/v12v13.py
+++ b/sipbuild/generator/outputs/code/backends/v12v13.py
@@ -10,17 +10,16 @@ from ....utils import find_method
 
 from ...formatters import fmt_argument_as_cpp_type
 
-from ..snippets import (g_class_docstring, g_class_method_table,
-        g_method_docstring, g_module_docstring, g_type_init_body, g_py_slot,
-        g_pyqt_class_plugin, g_pyqt_helper_defns, g_pyqt_helper_init,
-        g_static_function)
+from ..snippets import (g_class_docstring, g_method_docstring,
+        g_module_docstring, g_type_init_body, g_py_slot, g_pyqt_class_plugin,
+        g_pyqt_helper_defns, g_pyqt_helper_init, g_static_function)
 from ..utils import (get_class_flags, get_class_from_void, get_const_cast,
         get_docstring_text, get_encoded_type, get_enum_member,
-        get_function_table, get_mapped_type_flags, get_named_value_decl,
-        get_normalised_cached_name, get_optional_ptr, get_use_in_code,
-        get_user_state_suffix, get_void_ptr_cast, has_method_docstring,
-        is_used_in_code, keep_py_reference, need_dealloc, py_scope,
-        pyqt5_supported, pyqt6_supported, scoped_class_name,
+        get_function_table, get_mapped_type_flags, get_method_table,
+        get_named_value_decl, get_normalised_cached_name, get_optional_ptr,
+        get_use_in_code, get_user_state_suffix, get_void_ptr_cast,
+        has_method_docstring, is_used_in_code, keep_py_reference, need_dealloc,
+        py_scope, pyqt5_supported, pyqt6_supported, scoped_class_name,
         scoped_variable_name, type_needs_user_state, variables_in_scope)
 
 from .abstract_backend import AbstractBackend
@@ -112,6 +111,14 @@ f'''    if (targetType == {sc_type_ref})
             if iface_file.type is IfaceFileType.NAMESPACE:
                 sf.write('#endif\n')
 
+    def g_class_spec_extern_decl(self, sf, klass):
+        """ Generate the extern declaration of a class specification. """
+
+        module_name = self.spec.module.py_name
+        klass_name = klass.iface_file.fq_cpp_name.as_word
+
+        sf.write(f'\nextern sipClassTypeDef sipTypeDef_{module_name}_{klass_name};\n')
+
     def g_conversion_to_enum(self, sf, enum):
         """ Generate the code to convert a Python enum (sipSelf) to a C/C++
         enum (sipCpp).
@@ -589,7 +596,7 @@ f'''
         mapped_type_name = mapped_type.iface_file.fq_cpp_name.as_word
 
         members = get_function_table(mapped_type.members)
-        cod_nrmethods = self.g_py_method_table(sf, bindings, members,
+        cod_nrmethods = self._g_py_method_table(sf, bindings, members,
                 mapped_type)
 
         id_int = 'SIP_NULLPTR'
@@ -852,53 +859,6 @@ f'''
         if need_args:
             sf.write('    PyObject *sipParseErr = SIP_NULLPTR;\n');
 
-    def g_py_method_table(self, sf, bindings, members, scope):
-        """ Generate a Python method table for a class or mapped type and
-        return the number of entries.
-        """
-
-        scope_name = scope.iface_file.fq_cpp_name.as_word
-
-        no_intro = True
-
-        for member_nr, member in enumerate(members):
-            # Save the index in the table.
-            member.member_nr = member_nr
-
-            py_name = member.py_name
-            cached_py_name = self.cached_name_ref(py_name)
-            comma = '' if member is members[-1] else ','
-
-            if member.no_arg_parser or member.allow_keyword_args:
-                cast = 'SIP_MLMETH_CAST('
-                cast_suffix = ')'
-                flags = '|METH_KEYWORDS'
-            else:
-                cast = ''
-                cast_suffix = ''
-                flags = ''
-
-            if has_method_docstring(bindings, member, scope.overloads):
-                docstring = f'doc_{scope_name}_{py_name.name}'
-            else:
-                docstring = 'SIP_NULLPTR'
-
-            if no_intro:
-                sf.write(
-f'''
-
-static PyMethodDef methods_{scope_name}[] = {{
-''')
-
-                no_intro = False
-
-            sf.write(f'    {{{cached_py_name}, {cast}meth_{scope_name}_{py_name.name}{cast_suffix}, METH_VARARGS{flags}, {docstring}}}{comma}\n')
-
-        if not no_intro:
-            sf.write('};\n')
-
-        return len(members)
-
     def g_sip_api(self, sf, module_name, module_state):
         """ Generate the SIP API as seen by generated code. """
 
@@ -1321,7 +1281,7 @@ static sipPySlotDef slots_{klass_name}[] = {{
             sf.write('    {0, (sipPySlotType)0}\n};\n')
 
         # The attributes tables.
-        nr_methods = g_class_method_table(self, sf, bindings, klass)
+        nr_methods = self._g_class_method_table(sf, bindings, klass)
         nr_enum_members, _ = self.g_enums_specifications(sf, bindings,
                 scope=klass)
 
@@ -1798,6 +1758,18 @@ f'''static void *init_type_{klass_name}(sipSimpleWrapper *{sip_self}, PyObject *
 
         return docstring_ref
 
+    def _g_class_method_table(self, sf, bindings, klass):
+        """ Generate the sorted table of methods for a class and return the
+        number of entries.
+        """
+
+        if klass.iface_file.type is IfaceFileType.NAMESPACE:
+            members = get_function_table(klass.members)
+        else:
+            members = get_method_table(klass)
+
+        return self._g_py_method_table(sf, bindings, members, klass)
+
     def _g_enums_defs(self, sf, needed_enums):
         """ Generate the definitions for all wrapped enums. """
 
@@ -2234,6 +2206,53 @@ f'''
 
         return True
 
+    def _g_py_method_table(self, sf, bindings, members, scope):
+        """ Generate a Python method table for a class or mapped type and
+        return the number of entries.
+        """
+
+        scope_name = scope.iface_file.fq_cpp_name.as_word
+
+        no_intro = True
+
+        for member_nr, member in enumerate(members):
+            # Save the index in the table.
+            member.member_nr = member_nr
+
+            py_name = member.py_name
+            cached_py_name = self.cached_name_ref(py_name)
+            comma = '' if member is members[-1] else ','
+
+            if member.no_arg_parser or member.allow_keyword_args:
+                cast = 'SIP_MLMETH_CAST('
+                cast_suffix = ')'
+                flags = '|METH_KEYWORDS'
+            else:
+                cast = ''
+                cast_suffix = ''
+                flags = ''
+
+            if has_method_docstring(bindings, member, scope.overloads):
+                docstring = f'doc_{scope_name}_{py_name.name}'
+            else:
+                docstring = 'SIP_NULLPTR'
+
+            if no_intro:
+                sf.write(
+f'''
+
+static PyMethodDef methods_{scope_name}[] = {{
+''')
+
+                no_intro = False
+
+            sf.write(f'    {{{cached_py_name}, {cast}meth_{scope_name}_{py_name.name}{cast_suffix}, METH_VARARGS{flags}, {docstring}}}{comma}\n')
+
+        if not no_intro:
+            sf.write('};\n')
+
+        return len(members)
+
     # The types that are implemented as PyObject*.
     _PY_OBJECT_TYPES = (ArgumentType.PYOBJECT, ArgumentType.PYTUPLE,
         ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.PYCALLABLE,
diff --git a/sipbuild/generator/outputs/code/snippets.py b/sipbuild/generator/outputs/code/snippets.py
index 8c0f619..ecc9775 100644
--- a/sipbuild/generator/outputs/code/snippets.py
+++ b/sipbuild/generator/outputs/code/snippets.py
@@ -23,28 +23,15 @@ from ..formatters import (fmt_argument_as_cpp_type, fmt_argument_as_name,
 
 from .utils import (callable_overloads, get_class_from_void, get_const_cast,
         get_convert_to_type_code, get_docstring_text, get_encoded_type,
-        get_enum_class_scope, get_function_table, get_named_value_decl,
-        get_normalised_cached_name, get_optional_ptr, get_type_from_void,
-        get_use_in_code, get_user_state_suffix, get_void_ptr_cast,
-        has_method_docstring, is_used_in_code, keep_py_reference, need_dealloc,
-        need_error_flag, py_scope, pyqt5_supported, pyqt6_supported,
-        release_gil, scoped_class_name, skip_overload, type_needs_user_state,
+        get_enum_class_scope, get_named_value_decl, get_normalised_cached_name,
+        get_optional_ptr, get_type_from_void, get_use_in_code,
+        get_user_state_suffix, get_void_ptr_cast, has_method_docstring,
+        is_used_in_code, keep_py_reference, need_dealloc, need_error_flag,
+        py_scope, pyqt5_supported, pyqt6_supported, release_gil,
+        scoped_class_name, skip_overload, type_needs_user_state,
         variables_in_scope)
 
 
-def g_class_method_table(backend, sf, bindings, klass):
-    """ Generate the sorted table of methods for a class and return the number
-    of entries.
-    """
-
-    if klass.iface_file.type is IfaceFileType.NAMESPACE:
-        members = get_function_table(klass.members)
-    else:
-        members = _get_method_table(klass)
-
-    return backend.g_py_method_table(sf, bindings, members, klass)
-
-
 def g_composite_module_code(backend, sf, py_debug):
     """ Generate the code for a composite module. """
 
@@ -721,7 +708,7 @@ def _arg_is_v13_typed_enum(spec, arg):
     # passed to or from the ABI.  The returned value is used to determine if
     # the casting is necessary.  ABI v14 instead passes a pointer to the enum
     # value (rather than the value itself) which means that it can support enum
-    # types larger than an int and doesn't need any casting.  ABU v12 does not
+    # types larger than an int and doesn't need any casting.  ABI v12 does not
     # support typed enums.
 
     return spec.target_abi[0] == 13 and arg.type is ArgumentType.ENUM and arg.definition.enum_base_type is not None
@@ -781,7 +768,7 @@ def _arg_parser(backend, sf, scope, py_signature, signature_nr, ctor=None,
     single_arg = False
 
     if spec.target_abi >= (14, 0):
-        args.append('sipModule')
+        args.append('sipMS')
 
         if overload is not None:
             member_name = overload.common.py_name.name
@@ -1493,9 +1480,7 @@ def _class_api(backend, sf, klass):
     _enum_macros(backend, sf, scope=klass)
 
     if not klass.external and not klass.is_hidden_namespace:
-        klass_name = iface_file.fq_cpp_name.as_word
-        spec_suffix = backend.get_spec_suffix()
-        sf.write(f'\nextern sipClassType{spec_suffix} sipType{spec_suffix}_{module_name}_{klass_name};\n')
+        backend.g_class_spec_extern_decl(sf, klass)
 
 
 def g_class_docstring(sf, spec, bindings, klass):
@@ -1852,40 +1837,6 @@ def _gc_ellipsis(sf, signature):
         sf.write(f'\n            Py_DECREF(a{last});\n')
 
 
-def _get_method_table(klass):
-    """ Return a sorted list of relevant methods (either lazy or non-lazy) for
-    a class.
-    """
-
-    # Only provide an entry point if there is at least one overload that is
-    # defined in this class and is a non-abstract function or slot.  We allow
-    # private (even though we don't actually generate code) because we need to
-    # intercept the name before it reaches a more public version further up the
-    # class hierarchy.  We add the ctor and any variable handlers as special
-    # entries.
-
-    members = []
-
-    for visible_member in klass.visible_members:
-        if visible_member.member.py_slot is not None:
-            continue
-
-        need_member = False
-
-        for overload in visible_member.scope.overloads:
-            # Skip protected methods if we don't have the means to handle them.
-            if overload.access_specifier is AccessSpecifier.PROTECTED and not klass.has_shadow:
-                continue
-
-            if not skip_overload(overload, visible_member.member, klass, visible_member.scope):
-                need_member = True
-
-        if need_member:
-            members.append(visible_member.member)
-
-    return get_function_table(members)
-
-
 def _get_subformat_char(arg):
     """ Return the sub-format character for an argument. """
 
@@ -2717,7 +2668,7 @@ f'''    if (!PyObject_TypeCheck(sipSelf, sipTypeAsPyTypeObject(sip{prefix}_{fq_c
             if isinstance(scope, WrappedClass):
                 cpp_name = scoped_class_name(spec, scope)
                 type_ref = backend.get_type_ref(scope)
-                sip_module = 'sipModule, ' if spec.target_abi >= (14, 0) else ''
+                sip_module = 'sipMS, ' if spec.target_abi >= (14, 0) else ''
 
                 sf.write(
 f'''    {cpp_name} *sipCpp = reinterpret_cast<{cpp_name} *>(sipGetCppPtr({sip_module}{backend.get_wrapper_type_cast()}sipSelf, {type_ref}));
@@ -2780,7 +2731,7 @@ f'''
 
                 if is_number_slot(member.py_slot) or is_rich_compare_slot(member.py_slot):
                     if spec.target_abi >= (14, 0):
-                        extend_context = 'sipModule'
+                        extend_context = 'sipMS'
                     else:
                         extend_context = f'&sipModuleAPI_{spec.module.py_name}'
 
@@ -3893,7 +3844,14 @@ f'''
     sf.write(')\n{\n')
 
     if result_is_returned:
-        result_plain_decl = fmt_argument_as_cpp_type(spec, result, plain=True)
+        decl = fmt_argument_as_cpp_type(spec, result, plain=True)
+
+        if _arg_is_v13_typed_enum(spec, result):
+            result_plain_decl = 'int'
+            result_cast = '(' + decl + ')'
+        else:
+            result_plain_decl = decl
+            result_cast = ''
 
         if result_instance_code is not None:
             sf.write(
@@ -3995,8 +3953,8 @@ f'''
 
         if result_is_returned:
             sf.write(
-'''
-    return sipRes;
+f'''
+    return {result_cast}sipRes;
 ''')
 
         sf.write('}\n')
@@ -4097,7 +4055,7 @@ f'    PyObject *sipResObj = sipCallMethod({context}SIP_NULLPTR, sipMethod, ')
 
         sf.write(
 f'''
-    return {result_ref}sipRes;
+    return {result_cast}{result_ref}sipRes;
 ''')
 
     sf.write('}\n')
@@ -4955,7 +4913,7 @@ f'''            Py_INCREF(Py_None);
         build_result_args = []
 
         if spec.target_abi >= (14, 0):
-            build_result_args.append('sipModule')
+            build_result_args.append('sipMS')
 
         build_result_args.append('0')
 
diff --git a/sipbuild/generator/outputs/code/utils.py b/sipbuild/generator/outputs/code/utils.py
index ce75466..668c362 100644
--- a/sipbuild/generator/outputs/code/utils.py
+++ b/sipbuild/generator/outputs/code/utils.py
@@ -184,6 +184,40 @@ def get_mapped_type_flags(mapped_type):
     return '|'.join(flags)
 
 
+def get_method_table(klass):
+    """ Return a sorted list of relevant methods (either lazy or non-lazy) for
+    a class.
+    """
+
+    # Only provide an entry point if there is at least one overload that is
+    # defined in this class and is a non-abstract function or slot.  We allow
+    # private (even though we don't actually generate code) because we need to
+    # intercept the name before it reaches a more public version further up the
+    # class hierarchy.  We add the ctor and any variable handlers as special
+    # entries.
+
+    members = []
+
+    for visible_member in klass.visible_members:
+        if visible_member.member.py_slot is not None:
+            continue
+
+        need_member = False
+
+        for overload in visible_member.scope.overloads:
+            # Skip protected methods if we don't have the means to handle them.
+            if overload.access_specifier is AccessSpecifier.PROTECTED and not klass.has_shadow:
+                continue
+
+            if not skip_overload(overload, visible_member.member, klass, visible_member.scope):
+                need_member = True
+
+        if need_member:
+            members.append(visible_member.member)
+
+    return get_function_table(members)
+
+
 def get_named_value_decl(spec, scope, type, name):
     """ Return the declaration of a named variable to hold a C++ value. """
 
diff --git a/sipbuild/generator/parser/parser_manager.py b/sipbuild/generator/parser/parser_manager.py
index e857bfb..0731065 100644
--- a/sipbuild/generator/parser/parser_manager.py
+++ b/sipbuild/generator/parser/parser_manager.py
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: BSD-2-Clause
 
-# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2026 Phil Thompson <phil at riverbankcomputing.com>
 
 
 from functools import partial
@@ -605,7 +605,7 @@ class ParserManager:
         if base_type_s is not None:
             # The minor version of the target ABI may not be known yet (and we
             # don't need it) so just test the major version.
-            if self.spec.target_abi is not None and self.spec.target_abi[0] < 13:
+            if self.target_major_abi < 13:
                 self.parser_error(p, symbol,
                         "/BaseType/ is only supported for ABI v13.0 and later")
 
@@ -658,7 +658,7 @@ class ParserManager:
             # Check the member name if it is going to be visible in the current
             # scope.
             # TODO Also check for ABI v14 and custom enums.
-            if cpp_name is None or (self.spec.target_abi[0] == 12 and not is_scoped):
+            if cpp_name is None or (self.target_major_abi == 12 and not is_scoped):
                 self.check_attributes(p, symbol, m_py_name.name,
                         "an enum member")
 
@@ -1669,6 +1669,14 @@ class ParserManager:
 
         return value
 
+    @property
+    def target_major_abi(self):
+        """ The major version of the currently specified target ABI. """
+
+        target_abi = self.spec.target_abi
+
+        return DEFAULT_ABI_MAJOR if target_abi is None else target_abi[0]
+
     def validate_function(self, p, symbol, overload):
         """ Validate a completed function. """
 
@@ -1736,9 +1744,7 @@ class ParserManager:
     def validate_mapped_type(self, p, symbol, mapped_type):
         """ Validate a completed mapped type. """
 
-        # The minor version of the target ABI may not be known yet (and we
-        # don't need it) so just test the major version.
-        if self.spec.target_abi is None or self.spec.target_abi[0] >= 13:
+        if self.target_major_abi >= 13:
             convert_to_us = mapped_type.convert_to_type_code is not None and 'sipUserState' in mapped_type.convert_to_type_code.text
 
             release_us = mapped_type.release_code is not None and 'sipUserState' in mapped_type.release_code.text
@@ -1900,6 +1906,8 @@ class ParserManager:
         target_abi = self.spec.target_abi
 
         if target_abi is None:
+            deprecated("Not specifying %MinimumABIVersion")
+
             major_version = DEFAULT_ABI_MAJOR
             minor_version = None
         else:
diff --git a/sipbuild/generator/resolver/resolver.py b/sipbuild/generator/resolver/resolver.py
index b8deb24..7b3aab0 100644
--- a/sipbuild/generator/resolver/resolver.py
+++ b/sipbuild/generator/resolver/resolver.py
@@ -20,8 +20,8 @@ from ..specification import (AccessSpecifier, Argument, ArgumentType,
 from ..templates import (encoded_template_name, same_template_signature,
         template_code, template_code_blocks, template_expansions)
 from ..utils import (append_iface_file, argument_as_str, cached_name,
-        fast_contains, find_iface_file, find_method, same_argument_type,
-        same_base_type, same_signature, search_typedefs)
+        fast_contains, find_iface_file, find_method, is_namespace_extender,
+        same_argument_type, same_base_type, same_signature, search_typedefs)
 
 
 def resolve(spec, modules):
@@ -2179,9 +2179,14 @@ def _create_sorted_numbered_types(spec, mod, error_log):
             continue
 
         if mod is spec.module or klass.iface_file.needed:
-            if not klass.is_hidden_namespace:
-                mod.needed_types.append(Argument(ArgumentType.CLASS,
-                        definition=klass, name=klass.iface_file.cpp_name))
+            if klass.is_hidden_namespace:
+                continue
+
+            if spec.target_abi >= (14, 0) and is_namespace_extender(klass):
+                continue
+
+            mod.needed_types.append(Argument(ArgumentType.CLASS,
+                    definition=klass, name=klass.iface_file.cpp_name))
 
     for mapped_type in spec.mapped_types:
         if mapped_type.iface_file.module is not mod:
diff --git a/sipbuild/generator/utils.py b/sipbuild/generator/utils.py
index 5147ebf..76244fe 100644
--- a/sipbuild/generator/utils.py
+++ b/sipbuild/generator/utils.py
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: BSD-2-Clause
 
-# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2026 Phil Thompson <phil at riverbankcomputing.com>
 
 
 from .scoped_name import ScopedName
@@ -233,6 +233,12 @@ def find_method(klass, name):
     return None
 
 
+def is_namespace_extender(klass):
+    """ Return True if a class is a namespace extender. """
+
+    return klass.iface_file.type is IfaceFileType.NAMESPACE and klass.real_class is not None
+
+
 def normalised_scoped_name(scoped_name, scope):
     """ Convert a scoped name to a fully qualified name. """
 
diff --git a/test/conftest.py b/test/conftest.py
index 749ad67..c6e9de8 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -28,6 +28,15 @@ def pytest_addoption(parser):
             choices=ABI_VERSIONS, default=ABI_VERSIONS[-1])
 
 
+ at pytest.fixture
+def abi_package_version(package):
+    """ This fixture extracts the ABI version used to build a test package.
+    The value is the ABI major version.
+    """
+
+    return package.sip.SIP_ABI_VERSION >> 16
+
+
 @pytest.fixture
 def abi_version(module):
     """ This fixture extracts the ABI version used to build a test module.  The
@@ -292,7 +301,9 @@ def _build_test_module(sip_file, test_dir, abi_version, package, exceptions,
                 f.write(f'tags = [{tags_s}]\n')
 
     # Configure the C++11 support.
-    os.environ['CXXFLAGS'] = '-std=c++11'
+    cxxflags = os.environ.get('CXXFLAGS', '')
+    if '-std=c++11' not in cxxflags:
+        os.environ['CXXFLAGS'] = f'{cxxflags} -std=c++11'
 
     # Build and move the test module.
     _build_module(module_name, package,
diff --git a/test/extenders/core_module.sip b/test/extenders/core_module.sip
new file mode 100644
index 0000000..c492933
--- /dev/null
+++ b/test/extenders/core_module.sip
@@ -0,0 +1,26 @@
+// The SIP implementation of the core_module test module.
+
+
+%Module(name=extenders.core_module)
+
+
+%ModuleHeaderCode
+namespace NameSpace
+{
+    int twice(int n);
+};
+%End
+
+
+%ModuleCode
+int NameSpace::twice(int n)
+{
+    return 2 * n;
+}
+%End
+
+
+namespace NameSpace
+{
+    int twice(int n /Constrained/);
+};
diff --git a/test/extenders/extras1_module.sip b/test/extenders/extras1_module.sip
new file mode 100644
index 0000000..4ddbcaf
--- /dev/null
+++ b/test/extenders/extras1_module.sip
@@ -0,0 +1,35 @@
+// The SIP implementation of the extras1_module test module.
+
+
+%Module(name=extenders.extras1_module)
+
+%Import core_module.sip
+
+
+%ModuleHeaderCode
+namespace NameSpace
+{
+    float twice(float n);
+    float thrice(float n);
+};
+%End
+
+
+%ModuleCode
+float NameSpace::twice(float n)
+{
+    return 2 * n;
+}
+
+float NameSpace::thrice(float n)
+{
+    return 3 * n;
+}
+%End
+
+
+namespace NameSpace
+{
+    float twice(float n);
+    float thrice(float n);
+};
diff --git a/test/extenders/extras2_module.sip b/test/extenders/extras2_module.sip
new file mode 100644
index 0000000..5eb7d7a
--- /dev/null
+++ b/test/extenders/extras2_module.sip
@@ -0,0 +1,28 @@
+// The SIP implementation of the extras2_module test module.
+
+
+%Module(name=extenders.extras2_module)
+
+%Import core_module.sip
+
+
+%ModuleHeaderCode
+namespace NameSpace
+{
+    int thrice(int n);
+};
+%End
+
+
+%ModuleCode
+int NameSpace::thrice(int n)
+{
+    return 3 * n;
+}
+%End
+
+
+namespace NameSpace
+{
+    int thrice(int n /Constrained/);
+};
diff --git a/test/extenders/test_extenders.py b/test/extenders/test_extenders.py
new file mode 100644
index 0000000..d87cdf2
--- /dev/null
+++ b/test/extenders/test_extenders.py
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2026 Phil Thompson <phil at riverbankcomputing.com>
+
+
+import pytest
+
+
+cfg_package = 'extenders'
+
+
+def test_core_callable(package, abi_package_version):
+    # Older versions of the ABI do not support extending existing callables
+    # with new overloads.
+    if abi_package_version >= 14:
+        twice_i = package.core_module.NameSpace.twice(5)
+
+        assert isinstance(twice_i, int)
+        assert twice_i == 10
+
+def test_extended_overload(package):
+    twice_f = package.core_module.NameSpace.twice(5.0)
+
+    assert isinstance(twice_f, float)
+    assert twice_f == 10.0
+
+def test_extended_callable_or_overload(package, abi_package_version):
+    # Older versions of the ABI do not support extending existing callables
+    # with new overloads.
+    if abi_package_version >= 14:
+        thrice_i = package.core_module.NameSpace.thrice(5)
+
+        assert isinstance(thrice_i, int)
+        assert thrice_i == 15
+
+        thrice_f = package.core_module.NameSpace.thrice(5.0)
+
+        assert isinstance(thrice_f, float)
+        assert thrice_f == 15.0
diff --git a/test/imported_exceptions/handler_module.sip b/test/imported_exceptions/handler_module.sip
index 7d5e948..89baf1b 100644
--- a/test/imported_exceptions/handler_module.sip
+++ b/test/imported_exceptions/handler_module.sip
@@ -16,7 +16,7 @@
     SIP_BLOCK_THREADS
 
 #if SIP_ABI_MAJOR_VERSION >= 14
-    PyErr_SetString((PyObject *)sipGetPyType(sipModule, sipTypeID_std_exception), detail);
+    PyErr_SetString((PyObject *)sipGetPyType(sipMS, sipTypeID_std_exception), detail);
 #else
     PyErr_SetString(sipException_std_exception, detail);
 #endif
diff --git a/test/imports/core_module.sip b/test/imports/core_module.sip
index f482b9a..dc2854f 100644
--- a/test/imports/core_module.sip
+++ b/test/imports/core_module.sip
@@ -13,20 +13,3 @@ class BaseClass
 public:
     BaseClass();
 };
-
-
-%ModuleCode
-int NameSpace::get_core_value()
-{
-    return 10;
-}
-%End
-
-namespace NameSpace
-{
-%TypeHeaderCode
-#include "base_class.h"
-%End
-
-    int get_core_value();
-};
diff --git a/test/imports/extras_module.sip b/test/imports/extras_module.sip
index 4569618..455ea0c 100644
--- a/test/imports/extras_module.sip
+++ b/test/imports/extras_module.sip
@@ -14,11 +14,6 @@ class Subclass : public BaseClass
 public:
     Subclass() {}
 };
-
-namespace NameSpace
-{
-    int get_extras_value();
-};
 %End
 
 
@@ -27,20 +22,3 @@ class Subclass : public BaseClass
 public:
     Subclass();
 };
-
-
-%ModuleCode
-int NameSpace::get_extras_value()
-{
-    return 20;
-}
-%End
-
-namespace NameSpace
-{
-%TypeHeaderCode
-#include "base_class.h"
-%End
-
-    int get_extras_value();
-};
diff --git a/test/imports/test_imports.py b/test/imports/test_imports.py
index 1f930c8..53807ee 100644
--- a/test/imports/test_imports.py
+++ b/test/imports/test_imports.py
@@ -9,13 +9,6 @@ import pytest
 cfg_package = 'imports'
 
 
-def test_namespace_extenders(package):
-    assert package.core_module.NameSpace.get_core_value() == 10
-    assert package.core_module.NameSpace.get_extras_value() == 20
-
-    with pytest.raises(AttributeError):
-        package.extras_module.NameSpace
-
 def test_superclasses(package):
     assert issubclass(package.extras_module.Subclass,
             package.core_module.BaseClass)
diff --git a/test/movable/movable_module.sip b/test/movable/movable_module.sip
index 2265cfb..413df85 100644
--- a/test/movable/movable_module.sip
+++ b/test/movable/movable_module.sip
@@ -14,9 +14,9 @@ template <TYPE>
 
 %ConvertFromTypeCode
 #if SIP_ABI_MAJOR_VERSION >= 14
-    sipTypeID type_id = sipFindTypeID(sipModule, "TYPE*");
+    sipTypeID type_id = sipFindTypeID(sipMS, "TYPE*");
 
-    return sipConvertFromType(sipModule, sipCpp->release(), type_id, Py_None);
+    return sipConvertFromType(sipMS, sipCpp->release(), type_id, Py_None);
 #else
     const sipTypeDef *td = sipFindType("TYPE*");
 
@@ -26,18 +26,18 @@ template <TYPE>
 
 %ConvertToTypeCode
 #if SIP_ABI_MAJOR_VERSION >= 14
-    sipTypeID type_id = sipFindTypeID(sipModule, "TYPE");
+    sipTypeID type_id = sipFindTypeID(sipMS, "TYPE");
 
     if (sipIsErr == NULL)
-        return sipCanConvertToType(sipModule, sipPy, type_id, 0);
+        return sipCanConvertToType(sipMS, sipPy, type_id, 0);
 
     if (sipPy == Py_None)
         return 1;
 
     int state;
-    TYPE *t = reinterpret_cast<TYPE *>(sipConvertToType(sipModule, sipPy, type_id, sipTransferObj, 0, &state, sipIsErr));
+    TYPE *t = reinterpret_cast<TYPE *>(sipConvertToType(sipMS, sipPy, type_id, sipTransferObj, 0, &state, sipIsErr));
 
-    sipReleaseType(sipModule, t, type_id, state);
+    sipReleaseType(sipMS, t, type_id, state);
 
     if (*sipIsErr)
         return 0;
diff --git a/test/template_typedef/template_typedef_module.sip b/test/template_typedef/template_typedef_module.sip
index d7b480c..adf9c32 100644
--- a/test/template_typedef/template_typedef_module.sip
+++ b/test/template_typedef/template_typedef_module.sip
@@ -22,8 +22,8 @@ template<TYPE>
 
 %ConvertFromTypeCode
 #if SIP_ABI_MAJOR_VERSION >= 14
-    //sipTypeID kp_type_id = sipFindTypeID(sipModule, "TYPE");
-    sipTypeID kp_type_id = getSipType(sipModule, "TYPE");
+    //sipTypeID kp_type_id = sipFindTypeID(sipMS, "TYPE");
+    sipTypeID kp_type_id = getSipType(sipMS, "TYPE");
 
     if (!kp_type_id)
         return NULL;
@@ -48,7 +48,7 @@ template<TYPE>
         TYPE *cpp = new TYPE(sipCpp->at(i));
 
 #if SIP_ABI_MAJOR_VERSION >= 14
-        PyObject *pobj = sipConvertFromNewType(sipModule, cpp, kp_type_id,
+        PyObject *pobj = sipConvertFromNewType(sipMS, cpp, kp_type_id,
                 sipTransferObj);
 #else
         PyObject *pobj = sipConvertFromNewType(cpp, kpTypeDef, sipTransferObj);
@@ -70,8 +70,8 @@ template<TYPE>
 
 %ConvertToTypeCode
 #if SIP_ABI_MAJOR_VERSION >= 14
-    //sipTypeID kp_type_id = sipFindTypeID(sipModule, "TYPE");
-    sipTypeID kp_type_id = getSipType(sipModule, "TYPE");
+    //sipTypeID kp_type_id = sipFindTypeID(sipMS, "TYPE");
+    sipTypeID kp_type_id = getSipType(sipMS, "TYPE");
 
     if (!kp_type_id)
         return 0;
@@ -96,7 +96,7 @@ template<TYPE>
         int state;
 
 #if SIP_ABI_MAJOR_VERSION >= 14
-        TYPE *p = static_cast<TYPE *>(sipConvertToType(sipModule,
+        TYPE *p = static_cast<TYPE *>(sipConvertToType(sipMS,
                 PyList_GET_ITEM(sipPy, i), kp_type_id, sipTransferObj,
                 SIP_NOT_NONE, &state, sipIsErr));
 #else
@@ -107,7 +107,7 @@ template<TYPE>
 
         if (*sipIsErr) {
 #if SIP_ABI_MAJOR_VERSION >= 14
-            sipReleaseType(sipModule, p, kp_type_id, state);
+            sipReleaseType(sipMS, p, kp_type_id, state);
 #else
             sipReleaseType(p, kpTypeDef, state);
 #endif
@@ -118,7 +118,7 @@ template<TYPE>
         v->push_back(*p);
 
 #if SIP_ABI_MAJOR_VERSION >= 14
-        sipReleaseType(sipModule, p, kp_type_id, state);
+        sipReleaseType(sipMS, p, kp_type_id, state);
 #else
         sipReleaseType(p, kpTypeDef, state);
 #endif



More information about the Neon-commits mailing list