[neon/forks/sip6/Neon/release] /: New upstream version 6.13.0
Dmitry Shachnev
null at kde.org
Wed Dec 3 06:39:48 GMT 2025
Git commit 7e6be829fe501d008220d8b4e6a5c0914db9eee4 by Dmitry Shachnev.
Committed on 07/10/2025 at 21:01.
Pushed by carlosdem into branch 'Neon/release'.
New upstream version 6.13.0
M +3 -3 .git_archival.txt
M +11 -1 docs/annotations.rst
M +8 -0 docs/command_line_tools.rst
M +1 -1 docs/conf.py
M +43 -0 docs/directives.rst
M +5 -0 docs/pyproject_toml.rst
M +54 -0 docs/releases.md
M +1 -0 docs/specification_files.rst
M +2 -2 sipbuild/configurable.py
M +31 -20 sipbuild/distinfo/distinfo.py
M +1 -1 sipbuild/distutils_builder.py
M +12 -4 sipbuild/generator/outputs/code/code.py
M +1 -0 sipbuild/generator/parser/annotations.py
M +2 -0 sipbuild/generator/parser/parser_manager.py
M +19 -2 sipbuild/generator/parser/rules.py
M +2 -2 sipbuild/generator/parser/tokens.py
M +11 -1 sipbuild/generator/resolver/resolver.py
M +6 -0 sipbuild/generator/specification.py
M +37 -9 sipbuild/project.py
M +1 -1 sipbuild/setuptools_builder.py
M +6 -2 sipbuild/tools/distinfo.py
M +2 -2 test/README.md
A +9 -0 test/docstrings/docstrings_module.sip
A +17 -0 test/docstrings/test_docstrings.py
A +13 -0 test/gen_classes/gen_nonpublic_superclasses_module.sip
A +18 -0 test/gen_classes/test_gen_classes.py
M +8 -4 test/movable/test_movable.py
M +2 -1 test/utils/__init__.py
A +162 -0 test/utils/sip_base_test_case.py
A +32 -0 test/utils/sip_generator_test_case.py
M +7 -130 test/utils/sip_test_case.py
https://invent.kde.org/neon/forks/sip6/-/commit/7e6be829fe501d008220d8b4e6a5c0914db9eee4
diff --git a/.git_archival.txt b/.git_archival.txt
index 5c7f21c..977f84d 100644
--- a/.git_archival.txt
+++ b/.git_archival.txt
@@ -1,3 +1,3 @@
-node: 4f0ca7fd43b941311e070051f637d931e17fa5d7
-node-date: 2025-06-03T14:59:00+01:00
-describe-name: 6.12.0
+node: c4cab8396437b405b79d50565e87b37bffb046e0
+node-date: 2025-10-07T15:07:59+01:00
+describe-name: 6.13.0
diff --git a/docs/annotations.rst b/docs/annotations.rst
index eabbb91..4832a29 100644
--- a/docs/annotations.rst
+++ b/docs/annotations.rst
@@ -425,12 +425,22 @@ Class Annotations
This string annotation is used to specify the filename extension to be used
for the file containing the generated code for this class.
+
.. class-annotation:: ExportDerived
In many cases SIP generates a derived class for each class being wrapped
(see :ref:`ref-derived-classes`). Normally this is used internally. This
boolean annotation specifies that the declaration of the class is exported
- and able to be used by handwritten code.
+ and able to be used by handwritten code of any module.
+
+
+.. class-annotation:: ExportDerivedLocally
+
+ .. versionadded:: 6.13
+
+ This boolean annotation is similar to the :canno:`ExportDerived` class
+ annotation except that the declaration of the derived class is only
+ exported to handwritten code in the same module that the class is defined.
.. class-annotation:: External
diff --git a/docs/command_line_tools.rst b/docs/command_line_tools.rst
index d68889a..fff56c0 100644
--- a/docs/command_line_tools.rst
+++ b/docs/command_line_tools.rst
@@ -225,6 +225,14 @@ The full set of command line options is:
used to specify a particular version of a package project's :mod:`sip`
module. This option may be specified multiple times.
+.. option:: --sbom FILE
+
+ .. versionadded:: 6.13
+
+ ``FILE`` is copied to the :file:`sboms` subdirectory of the
+ :file:`.dist-info` directory as defined in PEP 770. ``FILE`` may be a
+ glob-style pattern. This option may be specified multiple times.
+
.. option:: --wheel-tag TAG
``TAG`` is written as the ``Tag`` in the :file:`WHEEL` file in the
diff --git a/docs/conf.py b/docs/conf.py
index 90bc454..71583b4 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.12.0'
+version = 'v6.13.0'
# -- General configuration ---------------------------------------------------
diff --git a/docs/directives.rst b/docs/directives.rst
index 10554c5..a393ba7 100644
--- a/docs/directives.rst
+++ b/docs/directives.rst
@@ -2235,6 +2235,49 @@ clashes of function names within a module in case the SIP ``-j`` command line
option is used.
+.. directive:: %TypeDerivedCode
+
+:directive:`%TypeDerivedCode`
+-----------------------------
+
+.. parsed-literal::
+ %TypeDerivedCode
+ *code*
+ %End
+
+.. versionadded:: 6.13
+
+In many cases SIP generates a derived class for each class being wrapped (see
+:ref:`ref-derived-classes`). This directive is used to specify handwritten
+code, typically the declaration of additional class members, to be included in
+the class's declaration. The code is placed at the start of the declaration in
+a public section.
+
+:directive:`%TypeCode` may be used to define the corresponding implementation
+of the new member if necessary.
+
+For example::
+
+ class Klass
+ {
+ %TypeDerivedCode
+ // Print the instance on stderr for debugging purposes.
+ void dump() const;
+ %End
+
+ %TypeCode
+ // The implementation.
+ void Klass::dump() const
+ {
+ fprintf(stderr,"Klass %s at %p\n", name(), this);
+ }
+ %End
+
+ // The rest of the class specification.
+
+ };
+
+
.. directive:: %TypeHeaderCode
:directive:`%TypeHeaderCode`
diff --git a/docs/pyproject_toml.rst b/docs/pyproject_toml.rst
index b1f61f2..9a957f1 100644
--- a/docs/pyproject_toml.rst
+++ b/docs/pyproject_toml.rst
@@ -225,6 +225,11 @@ list options may contain environment markers as defined in `PEP 508
target directory. By default the directory containing the Python
interpreter is used. There is also a corresponding command line option.
+**sbom-files**
+ The value is a list of files that are copied to the :file:`sboms`
+ subdirectory of the :file:`.dist-info` directory as defined in PEP 770. A
+ file may be a glob-style pattern.
+
**sdist-excludes**
The value is a list of files and directories, expressed as *glob* patterns
and relative to the directory containing the :file:`pyproject.toml` file,
diff --git a/docs/releases.md b/docs/releases.md
index 1ba077e..3ab6dcb 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1,5 +1,59 @@
# Release Notes
+## v6.13.0
+
+### `/ExportDerivedLocally/` class annotation
+
+The new `/ExportDerivedLocally/` class annotation is similar to the
+`/ExportDerived/` class annotation except that it only makes the derived
+class declaration available to handwritten code in the module in which the
+class was defined.
+
+### `%TypeDerivedCode` directive
+
+The new `%TypeDerivedCode` directive allows handwritten code to be specified
+that is included at the start of the declaration of a derived class.
+
+### Support for SBOMs
+
+The `tools.sip.project.sbom-files` key in `pyproject.toml` is used to
+specify a list of files (and glob-style patterns) that will be copied to
+the `.dist-info/sboms` directory as described in PEP 770.
+
+Resolves [#83](https://github.com/Python-SIP/sip/issues/83)
+
+### Improved error reporting
+
+When a build fails the last 100 lines of the build output will be
+displayed. As with previous versions the `--verbose` option must be
+specified to see the full build output.
+
+Resolves [#84](https://github.com/Python-SIP/sip/issues/84)
+
+### Support for bool-based enums
+
+Enums can now have `bool` as a base type.
+
+Resolves [#88](https://github.com/Python-SIP/sip/issues/88)
+
+### Non-public super-classes not supported
+
+An attempt to use non-public super-classes will now result in a deprecation
+message rather than being silently ignored. The super-class should simply
+be removed.
+
+### Bug fixes
+
+- Fixed the code generated for operator casts.
+- Fixed the handling of mapped types for C++ templates with `typedef`ed
+ arguments.
+- Fixed the test for the `/Movable/` annotation so that it works with
+ Python v3.14. Resolves [#82](https://github.com/Python-SIP/sip/issues/82)
+- A mis-named enum member was corrected. Resolves [#86](https://github.com/Python-SIP/sip/issues/86)
+- Specifying `%Docstring` as a sub-directive of the `%Module` directive
+ generated invalid code. Resolves [#81](https://github.com/Python-SIP/sip/issues/81)
+
+
## v6.12.0
### Support for C++11 enum base types
diff --git a/docs/specification_files.rst b/docs/specification_files.rst
index 85946d3..d6b6c98 100644
--- a/docs/specification_files.rst
+++ b/docs/specification_files.rst
@@ -88,6 +88,7 @@ file.
:directive:`%InstanceCode` |
:directive:`%PickleCode` |
:directive:`%TypeCode` |
+ :directive:`%TypeDerivedCode` |
:directive:`%TypeHeaderCode` |
:directive:`%TypeHintCode` |
*constructor* |
diff --git a/sipbuild/configurable.py b/sipbuild/configurable.py
index 6b22ab4..f400887 100644
--- a/sipbuild/configurable.py
+++ b/sipbuild/configurable.py
@@ -219,8 +219,8 @@ class Option:
attribute of a Configurable object. The value of the attribute can be set
either by __init__(), the pyproject.toml file and by the user using a
command line argument (in that order). Once the value is set it cannot be
- changed subsequently. For example, if an attribute is set
- in pyproject.toml then the user will not then be able to modify it from the
+ changed subsequently. For example, if an attribute is set in
+ pyproject.toml then the user will not then be able to modify it from the
command line. The value can only be changed from the command line if the
Option object has help text specified.
"""
diff --git a/sipbuild/distinfo/distinfo.py b/sipbuild/distinfo/distinfo.py
index 84ff026..2e67f09 100644
--- a/sipbuild/distinfo/distinfo.py
+++ b/sipbuild/distinfo/distinfo.py
@@ -19,9 +19,9 @@ from ..version import SIP_VERSION_STR
WHEEL_VERSION = '1.0'
-def distinfo(name, console_scripts, gui_scripts, generator, generator_version,
- inventory, metadata_overrides, prefix, project_root, requires_dists,
- wheel_tag):
+def distinfo(name, console_scripts, gui_scripts, sbom_files, generator,
+ generator_version, inventory, metadata_overrides, prefix, project_root,
+ requires_dists, wheel_tag):
""" Create and populate a .dist-info directory from an inventory file. """
if prefix is None:
@@ -50,12 +50,13 @@ def distinfo(name, console_scripts, gui_scripts, generator, generator_version,
# Create the directory.
create_distinfo(name, wheel_tag, installed, metadata, requires_dists,
- project_root, console_scripts, gui_scripts, prefix_dir=prefix,
- generator=generator, generator_version=generator_version)
+ project_root, console_scripts, gui_scripts, sbom_files,
+ prefix_dir=prefix, generator=generator,
+ generator_version=generator_version)
def create_distinfo(distinfo_dir, wheel_tag, installed, metadata,
- requires_dists, project_root, console_scripts, gui_scripts,
+ requires_dists, project_root, console_scripts, gui_scripts, sbom_files,
prefix_dir='', generator=None, generator_version=None):
""" Create and populate a .dist-info directory. """
@@ -87,26 +88,20 @@ def create_distinfo(distinfo_dir, wheel_tag, installed, metadata,
real_distinfo_dir),
str(e))
- # Reproducable builds.
- installed.sort()
-
- # Copy any license files.
+ # Copy any license and sbom files.
saved = os.getcwd()
os.chdir(project_root)
- license_root = os.path.join(distinfo_dir, 'licenses')
-
- for license_patt in metadata.get('license-file', []):
- for license_file in glob.glob(license_patt):
- license_fn = os.path.join(license_root, license_file)
- installed.append(license_fn)
-
- real_license_dir = prefix_dir + os.path.dirname(license_fn)
- os.makedirs(real_license_dir, exist_ok=True)
- shutil.copy(license_file, real_license_dir)
+ _install_files(metadata.get('license-file'), prefix_dir,
+ os.path.join(distinfo_dir, 'licenses'), installed)
+ _install_files(sbom_files, prefix_dir, os.path.join(distinfo_dir, 'sboms'),
+ installed)
os.chdir(saved)
+ # Reproducable builds.
+ installed.sort()
+
if wheel_tag is None:
# Create the INSTALLER file.
installer_fn = os.path.join(distinfo_dir, 'INSTALLER')
@@ -234,6 +229,22 @@ def write_metadata(metadata, requires_dists, metadata_fn, project_root,
metadata_f.write(description_f.read())
+def _install_files(files, prefix_dir, target_dir, installed):
+ """ Install a list of files into a subdirectory of the .dist-info
+ directory and update the list of installed files.
+ """
+
+ if files:
+ for patt in files:
+ for file in glob.glob(patt):
+ file_fn = os.path.join(target_dir, file)
+ installed.append(file_fn)
+
+ real_dir = prefix_dir + os.path.dirname(file_fn)
+ os.makedirs(real_dir, exist_ok=True)
+ shutil.copy(file, real_dir)
+
+
def _write_metadata_item(name, value, metadata_f):
""" Write a single metadata item. """
diff --git a/sipbuild/distutils_builder.py b/sipbuild/distutils_builder.py
index b52439c..18d10f4 100644
--- a/sipbuild/distutils_builder.py
+++ b/sipbuild/distutils_builder.py
@@ -83,7 +83,7 @@ class DistutilsBuilder(Builder):
create_distinfo(project.get_distinfo_dir(target_dir), wheel_tag,
installed, project.metadata, project.get_requires_dists(),
project.root_dir, project.console_scripts,
- project.gui_scripts)
+ project.gui_scripts, project.sbom_files)
def _build_extension_module(self, buildable):
""" Build an extension module from the sources. """
diff --git a/sipbuild/generator/outputs/code/code.py b/sipbuild/generator/outputs/code/code.py
index 3f9e452..19f704b 100644
--- a/sipbuild/generator/outputs/code/code.py
+++ b/sipbuild/generator/outputs/code/code.py
@@ -3399,7 +3399,7 @@ def _class_functions(sf, spec, bindings, klass, py_debug):
# Any shadow code.
if klass.has_shadow:
- if not klass.export_derived:
+ if not (klass.export_derived or klass.export_derived_locally):
_shadow_class_declaration(sf, spec, bindings, klass)
_shadow_code(sf, spec, bindings, klass)
@@ -4833,7 +4833,7 @@ def _get_parse_result_format(arg, spec, result_is_reference=False,
if arg.type is ArgumentType.PYOBJECT:
return 'O'
- if arg.type in (ArgumentType.PYTUPLE, ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.SLICE, ArgumentType.PYTYPE):
+ if arg.type in (ArgumentType.PYTUPLE, ArgumentType.PYLIST, ArgumentType.PYDICT, ArgumentType.PYSLICE, ArgumentType.PYTYPE):
return 'N' if arg.allow_none else 'T'
if arg.type is ArgumentType.PYBUFFER:
@@ -5075,6 +5075,10 @@ def _module_api(sf, spec, bindings):
if klass.iface_file.module is module:
_class_api(sf, spec, klass)
+ if klass.export_derived_locally:
+ sf.write_code(klass.iface_file.type_header_code)
+ _shadow_class_declaration(sf, spec, bindings, klass)
+
if klass.export_derived:
sf.write_code(klass.iface_file.type_header_code)
_shadow_class_declaration(sf, spec, bindings, klass)
@@ -5224,6 +5228,8 @@ class sip{klass_name} : public {_scoped_class_name(spec, klass)}
public:
''')
+ sf.write_code(klass.type_derived_code)
+
# Define a shadow class for any protected classes we have.
for protected_klass in spec.classes:
if not protected_klass.is_protected:
@@ -7375,7 +7381,9 @@ def _get_slot_call(spec, scope, overload, dereferenced):
return f'(*sipCpp)[{_get_slot_arg(spec, overload, 0)}]'
if py_slot in (PySlot.INT, PySlot.FLOAT):
- return '*sipCpp'
+ cpp_type = fmt_argument_as_cpp_type(spec,
+ overload.cpp_signature.result)
+ return cpp_type + '(*sipCpp)'
if py_slot is PySlot.ADD:
return _get_number_slot_call(spec, overload, '+')
@@ -8566,7 +8574,7 @@ def _module_docstring(sf, module):
if module.docstring is not None:
sf.write(
f'''
-"PyDoc_STRVAR(doc_mod_{module.py_name}, "{_docstring_text(module.docstring)}");
+PyDoc_STRVAR(doc_mod_{module.py_name}, "{_docstring_text(module.docstring)}");
''')
diff --git a/sipbuild/generator/parser/annotations.py b/sipbuild/generator/parser/annotations.py
index 2681577..f80f789 100644
--- a/sipbuild/generator/parser/annotations.py
+++ b/sipbuild/generator/parser/annotations.py
@@ -218,6 +218,7 @@ _ANNOTATION_TYPES = {
'DelayDtor': boolean(),
'DisallowNone': boolean(),
'ExportDerived': boolean(),
+ 'ExportDerivedLocally': boolean(),
'External': boolean(),
'Encoding': string(),
'Factory': boolean(),
diff --git a/sipbuild/generator/parser/parser_manager.py b/sipbuild/generator/parser/parser_manager.py
index e46a399..644dc85 100644
--- a/sipbuild/generator/parser/parser_manager.py
+++ b/sipbuild/generator/parser/parser_manager.py
@@ -123,6 +123,8 @@ class ParserManager:
klass.supertype = cached_name(self.spec, supertype)
klass.export_derived = annotations.get('ExportDerived', False)
+ klass.export_derived_locally = annotations.get('ExportDerivedLocally',
+ False)
klass.mixin = annotations.get('Mixin', False)
file_extension = annotations.get('FileExtension')
diff --git a/sipbuild/generator/parser/rules.py b/sipbuild/generator/parser/rules.py
index 3a45736..382f46c 100644
--- a/sipbuild/generator/parser/rules.py
+++ b/sipbuild/generator/parser/rules.py
@@ -1427,6 +1427,19 @@ def p_type_code(p):
pm.scope.type_code.append(p[2])
+# %TypeDerivedCode ############################################################
+
+def p_type_derived_code(p):
+ "type_derived_code : TypeDerivedCode CODE_BLOCK"
+
+ pm = p.parser.pm
+
+ if pm.skipping:
+ return
+
+ pm.scope.type_derived_code.append(p[2])
+
+
# %TypeHeaderCode #############################################################
def p_type_header_code(p):
@@ -1741,6 +1754,7 @@ _CLASS_ANNOTATIONS = (
'DelayDtor',
'Deprecated',
'ExportDerived',
+ 'ExportDerivedLocally',
'External',
'FileExtension',
'Metatype',
@@ -1882,6 +1896,9 @@ def p_superclass(p):
return
if p[1] != 'public':
+ # In SIP v7 this will be an error.
+ pm.deprecated(p, 1)
+
p[0] = None
return
@@ -1990,6 +2007,7 @@ def p_class_line(p):
| property
| release_buffer_code
| type_code
+ | type_derived_code
| type_header_code
| type_hint_code
| deprecated_code_directives CODE_BLOCK"""
@@ -3058,7 +3076,6 @@ def p_opt_namespace_body(p):
"""opt_namespace_body : '{' namespace_body '}'
| empty"""
-
def p_namespace_body(p):
"""namespace_body : namespace_statement
| namespace_body namespace_statement"""
@@ -3153,7 +3170,7 @@ _UNION_ANNOTATIONS = (
'AllowNone',
'DelayDtor',
'Deprecated',
- 'ExportDerived',
+ 'ExportDerivedLocally',
'External',
'FileExtension',
'Metatype',
diff --git a/sipbuild/generator/parser/tokens.py b/sipbuild/generator/parser/tokens.py
index ddb1aaa..1e76a4e 100644
--- a/sipbuild/generator/parser/tokens.py
+++ b/sipbuild/generator/parser/tokens.py
@@ -43,8 +43,8 @@ code_directives = {
'InitialisationCode', 'InstanceCode', 'MethodCode', 'ModuleCode',
'ModuleHeaderCode', 'PickleCode', 'PostInitialisationCode',
'PreInitialisationCode', 'PreMethodCode', 'RaiseCode', 'ReleaseCode',
- 'SetCode', 'TypeCode', 'TypeHeaderCode', 'TypeHintCode', 'UnitCode',
- 'UnitPostIncludeCode', 'VirtualCallCode', 'VirtualCatcherCode',
+ 'SetCode', 'TypeCode', 'TypeDerivedCode', 'TypeHeaderCode', 'TypeHintCode',
+ 'UnitCode', 'UnitPostIncludeCode', 'VirtualCallCode', 'VirtualCatcherCode',
'VirtualErrorHandler',
# Remove in SIP v7.
diff --git a/sipbuild/generator/resolver/resolver.py b/sipbuild/generator/resolver/resolver.py
index 9692ba5..4a6d067 100644
--- a/sipbuild/generator/resolver/resolver.py
+++ b/sipbuild/generator/resolver/resolver.py
@@ -943,6 +943,7 @@ _ENUM_BASE_TYPES = (
ArgumentType.USHORT,
ArgumentType.INT,
ArgumentType.UINT,
+ ArgumentType.BOOL,
)
def _resolve_enums(spec, error_log):
@@ -1782,6 +1783,15 @@ def _resolve_type(spec, mod, scope, type, error_log, allow_defined=False):
return
+ # Do a lightweight lookup of any template arguments that will resolve
+ # typedefs.
+ if type.type is ArgumentType.TEMPLATE:
+ for arg in type.definition.types.args:
+ if arg.type is ArgumentType.DEFINED:
+ _name_lookup(spec, mod, arg.definition, arg)
+ if arg.type is ArgumentType.NONE:
+ arg.type = ArgumentType.DEFINED
+
# See if the type refers to an instantiated template.
_resolve_instantiated_class_template(spec, type)
@@ -1933,7 +1943,7 @@ def _search_scope(spec, scope, scoped_name, type):
def _name_lookup(spec, mod, scoped_name, type):
- """ Look up a name and resole the corresponding type. """
+ """ Look up a name and resolve the corresponding type. """
_search_mapped_types(spec, mod, type, scoped_name)
if type.type is not ArgumentType.NONE:
diff --git a/sipbuild/generator/specification.py b/sipbuild/generator/specification.py
index 9daea9a..ffdf1b9 100644
--- a/sipbuild/generator/specification.py
+++ b/sipbuild/generator/specification.py
@@ -1516,6 +1516,9 @@ class WrappedClass:
# Set if /ExportDerived/ was specified.
export_derived: bool = False
+ # Set if /ExportDerivedLocally/ was specified.
+ export_derived_locally: bool = False
+
# Set if /External/ was specified.
external: bool = False
@@ -1636,6 +1639,9 @@ class WrappedClass:
# The %TypeCode.
type_code: list[CodeBlock] = field(default_factory=list)
+ # The %TypeDerivedCode.
+ type_derived_code: list[CodeBlock] = field(default_factory=list)
+
# The %TypeHintCode.
type_hint_code: Optional[CodeBlock] = None
diff --git a/sipbuild/project.py b/sipbuild/project.py
index faf6744..ed8ff43 100644
--- a/sipbuild/project.py
+++ b/sipbuild/project.py
@@ -4,6 +4,7 @@
import collections
+import locale
import os
import packaging
import shutil
@@ -70,16 +71,20 @@ class Project(AbstractProject, Configurable):
# The minor version number of the target Python installation.
Option('py_minor_version', option_type=int),
- # The name of the directory containing the .sip files. If the sip
- # module is shared then each set of bindings is in its own
- # sub-directory.
- Option('sip_files_dir', default='.'),
+ # The list of files and directories, specified as glob patterns, that
+ # should be included in the dist-info/sboms directory of a wheel.
+ Option('sbom_files', option_type=list),
# The list of files and directories, specified as glob patterns
# relative to the project directory, that should be excluded from an
# sdist.
Option('sdist_excludes', option_type=list),
+ # The name of the directory containing the .sip files. If the sip
+ # module is shared then each set of bindings is in its own
+ # sub-directory.
+ Option('sip_files_dir', default='.'),
+
# The list of additional directories to search for .sip files.
Option('sip_include_dirs', option_type=list),
@@ -463,7 +468,11 @@ class Project(AbstractProject, Configurable):
for rd in self.get_requires_dists():
args.append('--requires-dist')
- args.append('\\"{}\\"'.format(rd))
+ args.append(f'\\"{rd}\\"')
+
+ for sbom_file in self.sbom_files:
+ args.append('--sbom')
+ args.append(sbom_file)
for metadata, value in self._metadata_overrides.items():
if value:
@@ -594,7 +603,7 @@ class Project(AbstractProject, Configurable):
with subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=stderr) as pipe:
for line in pipe.stdout:
- yield str(line, encoding=sys.stdout.encoding)
+ yield str(line, encoding=locale.getpreferredencoding(), errors="ignore")
if pipe.returncode != 0 and fatal:
raise UserException(
@@ -603,10 +612,29 @@ class Project(AbstractProject, Configurable):
def run_command(self, args, *, fatal=True):
""" Run a command and display the output if requested. """
+ deck = None if self.verbose else collections.deque((), 100)
+
# Read stdout and stderr until there is no more output.
- for line in self.read_command_pipe(args, and_stderr=True, fatal=fatal):
- if self.verbose:
- sys.stdout.write(line)
+ nr_lines = 0
+
+ try:
+ for line in self.read_command_pipe(args, and_stderr=True, fatal=fatal):
+ nr_lines += 1
+
+ if deck is None:
+ sys.stdout.write(line)
+ else:
+ deck.append(line)
+ except UserException:
+ if deck is not None:
+ for line in deck:
+ sys.stdout.write(line)
+
+ if nr_lines > deck.maxlen:
+ sys.stdout.write(
+ "To see the full output use the --verbose option.\n")
+
+ raise
def setup(self, pyproject, tool, tool_description):
""" Complete the configuration of the project. """
diff --git a/sipbuild/setuptools_builder.py b/sipbuild/setuptools_builder.py
index fb8fbfc..1e12af1 100644
--- a/sipbuild/setuptools_builder.py
+++ b/sipbuild/setuptools_builder.py
@@ -80,7 +80,7 @@ class SetuptoolsBuilder(Builder):
create_distinfo(project.get_distinfo_dir(target_dir), wheel_tag,
installed, project.metadata, project.get_requires_dists(),
project.root_dir, project.console_scripts,
- project.gui_scripts)
+ project.gui_scripts, project.sbom_files)
def _build_extension_module(self, buildable):
""" Build an extension module from the sources. """
diff --git a/sipbuild/tools/distinfo.py b/sipbuild/tools/distinfo.py
index 0719e26..9c941c2 100644
--- a/sipbuild/tools/distinfo.py
+++ b/sipbuild/tools/distinfo.py
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: BSD-2-Clause
-# Copyright (c) 2024 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
from ..argument_parser import ArgumentParser
@@ -47,6 +47,9 @@ def main():
parser.add_argument('--requires-dist', dest='requires_dists',
action='append', help="additional Requires-Dist", metavar="EXPR")
+ parser.add_argument('--sbom', dest='sbom_files', action='append',
+ help="the name of an SBOM file", metavar='FILE')
+
parser.add_argument('--wheel-tag',
help="the tag if a wheel is being created", metavar="TAG")
@@ -57,7 +60,8 @@ def main():
try:
distinfo(name=args.names[0], console_scripts=args.console_scripts,
- gui_scripts=args.gui_scripts, generator=args.generator,
+ gui_scripts=args.gui_scripts, sbom_files=args.sbom_files,
+ generator=args.generator,
generator_version=args.generator_version,
inventory=args.inventory,
metadata_overrides=args.metadata_overrides, prefix=args.prefix,
diff --git a/test/README.md b/test/README.md
index 09ccf41..a4ccd88 100644
--- a/test/README.md
+++ b/test/README.md
@@ -17,8 +17,8 @@ To run the tests of a particular test case, run:
## Writing Unit Tests
-Each test sub-directory should contain an empty `__init__.py` file, a Python
-test script with the prefix `test_` and one or more `.sip` files.
+Each test sub-directory should contain a Python test script with the prefix
+`test_` and one or more `.sip` files.
The test script should contain a sub-class of `SIPTestCase`, which is imported
from the `utils` module. This will automatically build any modules (defined by
diff --git a/test/docstrings/docstrings_module.sip b/test/docstrings/docstrings_module.sip
new file mode 100644
index 0000000..ebdf51f
--- /dev/null
+++ b/test/docstrings/docstrings_module.sip
@@ -0,0 +1,9 @@
+// The SIP implementation of the docstrings_module test module.
+
+
+%Module(name=docstrings_module)
+{
+%Docstring
+Module
+%End
+};
diff --git a/test/docstrings/test_docstrings.py b/test/docstrings/test_docstrings.py
new file mode 100644
index 0000000..6681119
--- /dev/null
+++ b/test/docstrings/test_docstrings.py
@@ -0,0 +1,17 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPTestCase
+
+
+class DocstringsTestCase(SIPTestCase):
+ """ Test the support for docstrings. """
+
+ def test_ModuleDocstrings(self):
+ """ Test the support for timelines. """
+
+ import docstrings_module as dm
+
+ self.assertEqual(dm.__doc__, 'Module')
diff --git a/test/gen_classes/gen_nonpublic_superclasses_module.sip b/test/gen_classes/gen_nonpublic_superclasses_module.sip
new file mode 100644
index 0000000..c8e4c49
--- /dev/null
+++ b/test/gen_classes/gen_nonpublic_superclasses_module.sip
@@ -0,0 +1,13 @@
+// The SIP implementation of the gen_nonpublic_superclasses_module test module.
+
+
+%Module(name=gen_nonpublic_superclasses_module)
+
+
+class Bar
+{
+};
+
+class Foo : protected Bar
+{
+};
diff --git a/test/gen_classes/test_gen_classes.py b/test/gen_classes/test_gen_classes.py
new file mode 100644
index 0000000..f54bb69
--- /dev/null
+++ b/test/gen_classes/test_gen_classes.py
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPGeneratorTestCase
+
+
+class GenerateClassesTestCase(SIPGeneratorTestCase):
+ """ Test the generator support for classes. """
+
+ def test_Nonpublic_Superclasses(self):
+ """ Test the support non-public super-classes. """
+
+ # Check that the use of non-public super-classes fails.
+ self.assertFalse(
+ self.run_generator_test(
+ 'gen_nonpublic_superclasses_module.sip'))
diff --git a/test/movable/test_movable.py b/test/movable/test_movable.py
index 04aadb8..fbe3f74 100644
--- a/test/movable/test_movable.py
+++ b/test/movable/test_movable.py
@@ -22,26 +22,30 @@ class MovableTestCase(SIPTestCase):
ao = AnObject(3)
ow = ObjectWrapper()
+ # As of Python v3.14 we can't make assumptions about initial
+ # reference counts so we test for increases and decreases rather than
+ # absolute values.
+ ao_base_refcount = getrefcount(ao)
+
# Test the value of the object.
self.assertEqual(ao.getValue(), 3)
- self.assertEqual(getrefcount(ao), 2)
+ self.assertEqual(getrefcount(ao), ao_base_refcount)
# Test an empty wrapper.
self.assertEqual(ow.getObjectValue(), -1000)
# Test an non-empty wrapper.
ow.setObject(ao)
- self.assertEqual(getrefcount(ao), 3)
+ self.assertEqual(getrefcount(ao), ao_base_refcount + 1)
self.assertEqual(ow.getObjectValue(), 4)
# Unwrap the object and test the wrapper.
ao2 = ow.takeObject()
- self.assertEqual(getrefcount(ao2), 2)
self.assertEqual(ow.getObjectValue(), -1000)
# Re-test the value of the object.
self.assertEqual(ao2.getValue(), 4)
# Check that the original Python object no longer wraps the C++ object.
- self.assertEqual(getrefcount(ao), 2)
+ self.assertEqual(getrefcount(ao), ao_base_refcount)
self.assertRaises(RuntimeError, ao.getValue)
diff --git a/test/utils/__init__.py b/test/utils/__init__.py
index ff04a52..2f9f271 100644
--- a/test/utils/__init__.py
+++ b/test/utils/__init__.py
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: BSD-2-Clause
-# Copyright (c) 2024 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+from .sip_generator_test_case import SIPGeneratorTestCase
from .sip_test_case import SIPTestCase
diff --git a/test/utils/sip_base_test_case.py b/test/utils/sip_base_test_case.py
new file mode 100644
index 0000000..9ec8cf5
--- /dev/null
+++ b/test/utils/sip_base_test_case.py
@@ -0,0 +1,162 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+import glob
+import inspect
+import os
+import shutil
+import subprocess
+import sys
+import unittest
+
+
+class SIPBaseTestCase(unittest.TestCase):
+ """ The base class for all test cases. """
+
+ # The ABI version to use. None implies the latest major version.
+ abi_version = None
+
+ # Set if exception support should be enabled.
+ exceptions = False
+
+ # The list of tags to be used to configure the test modules.
+ tags = None
+
+ @classmethod
+ def get_test_root_directory(cls):
+ """ Get the name of the test's root directory from the file
+ implementing the test case.
+ """
+
+ return os.path.dirname(inspect.getfile(cls))
+
+ @classmethod
+ def build_test_module(cls, sip_file, root_dir, use_sip_module=False,
+ no_compile=False):
+ """ Build a test extension module and return its name. """
+
+ build_dir = sip_file[:-4]
+ module_name = os.path.basename(build_dir)
+
+ # Remove any previous build directory.
+ shutil.rmtree(build_dir, ignore_errors=True)
+
+ os.mkdir(build_dir)
+
+ # Create a pyproject.toml file.
+ pyproject_toml = os.path.join(build_dir, 'pyproject.toml')
+
+ with open(pyproject_toml, 'w') as f:
+ f.write(_PYPROJECT_TOML.format(module_name=module_name))
+
+ if cls.abi_version is not None:
+ f.write(_ABI_VERSION.format(abi_version=cls.abi_version))
+
+ if use_sip_module:
+ f.write(_SIP_MODULE)
+
+ if cls.tags is not None or cls.exceptions:
+ f.write(f'\n[tool.sip.bindings.{module_name}]\n')
+
+ if cls.tags is not None:
+ tags_s = ', '.join([f'"{t}"' for t in cls.tags])
+ f.write(f'tags = [{tags_s}]\n')
+
+ if cls.exceptions:
+ f.write('exceptions = true\n')
+
+ # Build and move the test module.
+ cls.build_module(module_name,
+ ['-m', 'sipbuild.tools.build', '--verbose'], build_dir,
+ root_dir, impl_subdirs=[module_name, 'build'],
+ no_compile=no_compile)
+
+ return module_name
+
+ @classmethod
+ def build_module(cls, module_name, build_args, src_dir, root_dir,
+ impl_subdirs=None, no_compile=False):
+ """ Build a module and move any implementation to the test's root
+ directory.
+ """
+
+ cwd = os.getcwd()
+ os.chdir(src_dir)
+
+ # Do the build.
+ args = [sys.executable] + build_args
+
+ if no_compile:
+ args.append('--no-compile')
+
+ subprocess.run(args).check_returncode()
+
+ if no_compile:
+ return
+
+ # Find the implementation. Note that we only support setuptools and
+ # not distutils.
+ impl_pattern = ['build']
+
+ if impl_subdirs is not None:
+ impl_pattern.extend(impl_subdirs)
+
+ impl_pattern.append('lib*')
+ impl_pattern.append(
+ module_name + '*.pyd' if sys.platform == 'win32' else '*.so')
+
+ impl_paths = glob.glob(os.path.join(*impl_pattern))
+ if len(impl_paths) == 0:
+ raise Exception(
+ "no '{0}' extension module was built".format(module_name))
+
+ if len(impl_paths) != 1:
+ raise Exception(
+ "unable to determine file name of the '{0}' extension module".format(module_name))
+
+ impl_path = os.path.abspath(impl_paths[0])
+ impl = os.path.basename(impl_path)
+
+ os.chdir(root_dir)
+
+ try:
+ os.remove(impl)
+ except:
+ pass
+
+ os.rename(impl_path, impl)
+
+ os.chdir(cwd)
+
+ @classmethod
+ def get_test_root_directory(cls):
+ """ Get the name of the test's root directory from the file
+ implementing the test case.
+ """
+
+ return os.path.dirname(inspect.getfile(cls))
+
+
+# The prototype pyproject.toml file.
+_PYPROJECT_TOML = """
+[build-system]
+requires = ["sip >=6"]
+build-backend = "sipbuild.api"
+
+[project]
+name = "{module_name}"
+
+[tool.sip.project]
+minimum-macos-version = "10.9"
+sip-files-dir = ".."
+"""
+
+_ABI_VERSION = """
+abi-version = "{abi_version}"
+"""
+
+_SIP_MODULE = """
+sip-module = "sip"
+"""
diff --git a/test/utils/sip_generator_test_case.py b/test/utils/sip_generator_test_case.py
new file mode 100644
index 0000000..81c99ce
--- /dev/null
+++ b/test/utils/sip_generator_test_case.py
@@ -0,0 +1,32 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+import os
+import subprocess
+
+
+from .sip_base_test_case import SIPBaseTestCase
+
+
+class SIPGeneratorTestCase(SIPBaseTestCase):
+ """ Encapsulate a test case that tests the generation (but not the build or
+ execution) of a number of test modules.
+ """
+
+ @classmethod
+ def run_generator_test(cls, sip_file):
+ """ Run a test that generates a test module and return True if it was
+ successfully generated.
+ """
+
+ root_dir = cls.get_test_root_directory()
+
+ try:
+ cls.build_test_module(os.path.join(root_dir, sip_file), root_dir,
+ no_compile=True)
+ except subprocess.CalledProcessError:
+ return False
+
+ return True
diff --git a/test/utils/sip_test_case.py b/test/utils/sip_test_case.py
index c3310e9..4c7e9da 100644
--- a/test/utils/sip_test_case.py
+++ b/test/utils/sip_test_case.py
@@ -4,26 +4,16 @@
import glob
-import inspect
import os
-import shutil
import subprocess
import sys
import tarfile
-import unittest
+from .sip_base_test_case import SIPBaseTestCase
-class SIPTestCase(unittest.TestCase):
- """ Encapsulate a test case that tests a set of standalone bindings. """
- # The ABI version to use. None implies the latest major version.
- abi_version = None
-
- # Set if exception support should be enabled.
- exceptions = False
-
- # The list of tags to be used to configure the test modules.
- tags = None
+class SIPTestCase(SIPBaseTestCase):
+ """ Encapsulate a test case that tests the execution of bindings. """
# Set if a separate sip module should be generated. It will be built
# automatically if more than one module is being built.
@@ -33,9 +23,7 @@ class SIPTestCase(unittest.TestCase):
def setUpClass(cls):
""" Build a test extension module. """
- # Get the name of the test's root directory from the file implementing
- # the test case.
- root_dir = os.path.dirname(inspect.getfile(cls))
+ root_dir = cls.get_test_root_directory()
cls._modules = []
@@ -48,7 +36,8 @@ class SIPTestCase(unittest.TestCase):
for sip_file in sip_files:
cls._modules.append(
- cls._build_test_module(sip_file, use_sip_module, root_dir))
+ cls.build_test_module(sip_file, root_dir,
+ use_sip_module=use_sip_module))
# Fix the path.
sys.path.insert(0, root_dir)
@@ -94,47 +83,6 @@ class SIPTestCase(unittest.TestCase):
# Save the exception for later.
self._exc = xvalue
- @classmethod
- def _build_test_module(cls, sip_file, use_sip_module, root_dir):
- """ Build a test extension module and return its name. """
-
- build_dir = sip_file[:-4]
- module_name = os.path.basename(build_dir)
-
- # Remove any previous build directory.
- shutil.rmtree(build_dir, ignore_errors=True)
-
- os.mkdir(build_dir)
-
- # Create a pyproject.toml file.
- pyproject_toml = os.path.join(build_dir, 'pyproject.toml')
-
- with open(pyproject_toml, 'w') as f:
- f.write(_PYPROJECT_TOML.format(module_name=module_name))
-
- if cls.abi_version is not None:
- f.write(_ABI_VERSION.format(abi_version=cls.abi_version))
-
- if use_sip_module:
- f.write(_SIP_MODULE)
-
- if cls.tags is not None or cls.exceptions:
- f.write(f'\n[tool.sip.bindings.{module_name}]\n')
-
- if cls.tags is not None:
- tags_s = ', '.join([f'"{t}"' for t in cls.tags])
- f.write(f'tags = [{tags_s}]\n')
-
- if cls.exceptions:
- f.write('exceptions = true\n')
-
- # Build and move the test module.
- cls._build_module(module_name,
- ['-m', 'sipbuild.tools.build', '--verbose'], build_dir,
- root_dir, impl_subdirs=[module_name, 'build'])
-
- return module_name
-
@classmethod
def _build_sip_module(cls, root_dir):
""" Build the sip module and return its name. """
@@ -170,78 +118,7 @@ class SIPTestCase(unittest.TestCase):
# Build and move the module.
src_dir = os.path.join(root_dir, os.path.basename(sdist)[:-7])
- cls._build_module(sip_module_name, ['setup.py', 'build'], src_dir,
+ cls.build_module(sip_module_name, ['setup.py', 'build'], src_dir,
root_dir)
return sip_module_name
-
- @classmethod
- def _build_module(cls, module_name, build_args, src_dir, root_dir,
- impl_subdirs=None):
- """ Build a module and move the implementation to the test's root
- directory.
- """
-
- cwd = os.getcwd()
- os.chdir(src_dir)
-
- # Do the build.
- args = [sys.executable] + build_args
- subprocess.run(args).check_returncode()
-
- # Find the implementation. Note that we only support setuptools and
- # not distutils.
- impl_pattern = ['build']
-
- if impl_subdirs is not None:
- impl_pattern.extend(impl_subdirs)
-
- impl_pattern.append('lib*')
- impl_pattern.append(
- module_name + '*.pyd' if sys.platform == 'win32' else '*.so')
-
- impl_paths = glob.glob(os.path.join(*impl_pattern))
- if len(impl_paths) == 0:
- raise Exception(
- "no '{0}' extension module was built".format(module_name))
-
- if len(impl_paths) != 1:
- raise Exception(
- "unable to determine file name of the '{0}' extension module".format(module_name))
-
- impl_path = os.path.abspath(impl_paths[0])
- impl = os.path.basename(impl_path)
-
- os.chdir(root_dir)
-
- try:
- os.remove(impl)
- except:
- pass
-
- os.rename(impl_path, impl)
-
- os.chdir(cwd)
-
-
-# The prototype pyproject.toml file.
-_PYPROJECT_TOML = """
-[build-system]
-requires = ["sip >=6"]
-build-backend = "sipbuild.api"
-
-[project]
-name = "{module_name}"
-
-[tool.sip.project]
-minimum-macos-version = "10.9"
-sip-files-dir = ".."
-"""
-
-_ABI_VERSION = """
-abi-version = "{abi_version}"
-"""
-
-_SIP_MODULE = """
-sip-module = "sip"
-"""
More information about the Neon-commits
mailing list