[neon/forks/sip6/Neon/release] /: New upstream version 6.11.0
Dmitry Shachnev
null at kde.org
Sun Aug 17 07:48:50 BST 2025
Git commit f9e7c168b2e497480208b72d2182b6052070660d by Dmitry Shachnev.
Committed on 21/05/2025 at 21:00.
Pushed by carlosdem into branch 'Neon/release'.
New upstream version 6.11.0
M +3 -3 .git_archival.txt
M +4 -0 .gitignore
M +1 -1 LICENSE
M +4 -6 docs/abi_12.rst
M +4 -6 docs/abi_13.rst
M +18 -0 docs/annotations.rst
M +1 -1 docs/conf.py
M +7 -2 docs/directives.rst
M +97 -0 docs/releases.md
M +4 -1 docs/specification_files.rst
M +4 -4 pyproject.toml
M +1 -1 sipbuild/bindings.py
M +1 -2 sipbuild/buildable.py
M +16 -9 sipbuild/builder.py
M +19 -1 sipbuild/distinfo/distinfo.py
M +126 -24 sipbuild/generator/instantiations.py
M +17 -5 sipbuild/generator/outputs/code/code.py
M +5 -1 sipbuild/generator/outputs/formatters/template.py
M +2 -1 sipbuild/generator/parser/annotations.py
M +20 -13 sipbuild/generator/parser/parser_manager.py
M +55 -16 sipbuild/generator/parser/rules.py
M +1 -1 sipbuild/generator/python_slots.py
M +6 -0 sipbuild/generator/resolver/resolver.py
M +8 -4 sipbuild/generator/specification.py
M +1 -1 sipbuild/generator/utils.py
M +1 -1 sipbuild/module/source/12/16/sip.h.in
M +12 -8 sipbuild/module/source/12/16/siplib.c
M +1 -1 sipbuild/module/source/12/17/sip.h.in
M +13 -9 sipbuild/module/source/12/17/siplib.c
M +1 -1 sipbuild/module/source/13/10/sip.h.in
M +13 -9 sipbuild/module/source/13/10/sip_core.c
M +1 -1 sipbuild/module/source/13/9/sip.h.in
M +12 -8 sipbuild/module/source/13/9/sip_core.c
M +45 -22 sipbuild/project.py
M +4 -4 sipbuild/py_versions.py
M +47 -8 sipbuild/pyproject.py
A +31 -0 test/README.md
A +3 -0 test/imported_exceptions/__init__.py
A +19 -0 test/imported_exceptions/handler_module.sip
A +21 -0 test/imported_exceptions/test_imported_exceptions.py
A +19 -0 test/imported_exceptions/thrower_module.sip
R +4 -1 test/int_convertors/int_convertors_module.sip [from: test/int_convertors/int_convertors.sip - 098% similarity]
M +2 -2 test/int_convertors/test_int_convertors.py
C +0 -0 test/movable/__init__.py [from: test/enums/__init__.py - 100% similarity]
A +127 -0 test/movable/movable_module.sip
A +47 -0 test/movable/test_movable.py
R +0 -0 test/py_enums/__init__.py [from: test/enums/__init__.py - 100% similarity]
R +3 -2 test/py_enums/py_enums_module.sip [from: test/enums/enums.sip - 096% similarity]
R +25 -22 test/py_enums/test_py_enums.py [from: test/enums/test_enums.py - 085% similarity]
A +3 -0 test/template_superclasses/__init__.py
A +71 -0 test/template_superclasses/template_superclasses_module.sip
A +26 -0 test/template_superclasses/test_template_superclasses.py
A +3 -0 test/timelines/__init__.py
A +28 -0 test/timelines/test_timelines.py
A +50 -0 test/timelines/timelines_module.sip
M +170 -79 test/utils/sip_test_case.py
https://invent.kde.org/neon/forks/sip6/-/commit/f9e7c168b2e497480208b72d2182b6052070660d
diff --git a/.git_archival.txt b/.git_archival.txt
index 9438b99..3f3a819 100644
--- a/.git_archival.txt
+++ b/.git_archival.txt
@@ -1,3 +1,3 @@
-node: 350f56b80fe8a4d027ad6b301be607cc8c921a19
-node-date: 2025-02-02T13:19:13Z
-describe-name: 6.10.0
+node: b066f3f87f6e969b972f049cc752e09b3f609811
+node-date: 2025-05-16T12:57:02+01:00
+describe-name: 6.11.0
diff --git a/.gitignore b/.gitignore
index 5f52aa0..ba1be36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
# Local additions to Python.gitignore from GitHub.
sipbuild/_version.py
+test/*/*.pyd
+test/*/*.so
+test/*/*.tar.gz
+test/*/*/*
# Byte-compiled / optimized / DLL files
__pycache__/
diff --git a/LICENSE b/LICENSE
index 0e4caa6..12c2461 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2024 Phil Thompson <phil at riverbankcomputing.com>
+Copyright 2025 Phil Thompson <phil at riverbankcomputing.com>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
diff --git a/docs/abi_12.rst b/docs/abi_12.rst
index 6467ca8..17d4482 100644
--- a/docs/abi_12.rst
+++ b/docs/abi_12.rst
@@ -5,18 +5,16 @@ In this section we describe the v12 of the ABI, provided by the :mod:`sip`
module, that can be used by handwritten code in specification files.
-.. c:macro:: SIP_API_MAJOR_NR
+.. c:macro:: SIP_ABI_MAJOR_VERSION
This is a C preprocessor symbol that defines the major number of the SIP
- API. Its value is a number. There is no direct relationship between this
- and the SIP version number.
+ ABI.
-.. c:macro:: SIP_API_MINOR_NR
+.. c:macro:: SIP_ABI_MINOR_VERSION
This is a C preprocessor symbol that defines the minor number of the SIP
- API. Its value is a number. There is no direct relationship between this
- and the SIP version number.
+ ABI.
.. c:macro:: SIP_BLOCK_THREADS
diff --git a/docs/abi_13.rst b/docs/abi_13.rst
index 46fef4a..32c2378 100644
--- a/docs/abi_13.rst
+++ b/docs/abi_13.rst
@@ -4,18 +4,16 @@ ABI v13 for Handwritten Code
In this section we describe the v13 of the ABI, provided by the :mod:`sip`
module, that can be used by handwritten code in specification files.
-.. c:macro:: SIP_API_MAJOR_NR
+.. c:macro:: SIP_ABI_MAJOR_VERSION
This is a C preprocessor symbol that defines the major number of the SIP
- API. Its value is a number. There is no direct relationship between this
- and the SIP version number.
+ ABI.
-.. c:macro:: SIP_API_MINOR_NR
+.. c:macro:: SIP_ABI_MINOR_VERSION
This is a C preprocessor symbol that defines the minor number of the SIP
- API. Its value is a number. There is no direct relationship between this
- and the SIP version number.
+ ABI.
.. c:macro:: SIP_BLOCK_THREADS
diff --git a/docs/annotations.rst b/docs/annotations.rst
index 88c3b1e..eabbb91 100644
--- a/docs/annotations.rst
+++ b/docs/annotations.rst
@@ -558,6 +558,24 @@ Mapped Type Annotations
specifies that the handling of ``None`` will be left to the
:directive:`%ConvertToTypeCode`.
+.. mapped-type-annotation:: Movable
+
+ .. versionadded:: 6.11
+
+ If a C++ instance is passed by value as an argument to a function then the
+ class's assignment operator is normally used under the covers. If the
+ class's assignment operator has been deleted then the generated code will
+ not compile. This annotation says that the C++ instances of this type
+ should be moved instead, ie. the argument should be wrapped in a called to
+ `std::move()`. Because the C++ instance is unusable after being passed to
+ `std::move()`, SIP automatically transfers ownership of the instance to C++
+ so that Python doesn't try to call its destructor.
+
+ .. note::
+ SIP does not automatically generate the declaration of `std::move()`
+ so the :directive:`%TypeHeaderCode` for the mapped type should include
+ `#include <utility>`.
+
.. mapped-type-annotation:: NoAssignmentOperator
diff --git a/docs/conf.py b/docs/conf.py
index 4c56130..b902c26 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.10.0'
+version = 'v6.11.0'
# -- General configuration ---------------------------------------------------
diff --git a/docs/directives.rst b/docs/directives.rst
index 6dbe505..10554c5 100644
--- a/docs/directives.rst
+++ b/docs/directives.rst
@@ -653,7 +653,7 @@ For example::
%End
This directive is used to specify explicit docstrings for modules, classes,
-functions, methods, typedefs and properties.
+namespaces, functions, methods, typedefs and properties.
The docstring of a class is made up of the docstring specified for the class
itself, with the docstrings specified for each contructor appended.
@@ -1800,7 +1800,12 @@ limited Python API defined in `PEP 384
<https://www.python.org/dev/peps/pep-0384/>`__. It also ensures that the C
preprocessor symbol ``Py_LIMITED_API`` is defined before the :file:`Python.h`
header file is included. Python extensions built in this way are independent
-of the version of Python being used.
+of the version of Python being used. The version of the limited API to use
+(ie. the value assigned to the ``Py_LIMITED_API`` macro) is determined by the
+minimum version of Python supported by the project, ie. the value of the
+``requires-python`` field of the metadata specified in the ``pyproject.toml``
+file. If this isn't specified then the oldest supported version of Python is
+used.
The optional :directive:`%AutoPyName` sub-directive is used to specify a rule
for automatically providing Python names.
diff --git a/docs/releases.md b/docs/releases.md
index 1a4b055..b0fd1e3 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1,6 +1,103 @@
# Release Notes
+## v6.11.0
+
+### Added the `/Movable/` mapped type annotation
+
+When the `/Movable/` annotation is specified for a mapped type, values of that
+type are wrapped in calls to `std::move()` when passing them as arguments to a
+C++ callable. In addition the `/Transfer/` argument annotation is
+automatically applied.
+
+Resolves [#60](https://github.com/Python-SIP/sip/issues/60)
+
+### Support for template arguments in super-classes
+
+It is now possible to invoke a class template as a super-class in a class
+template.
+
+Resolves [#12](https://github.com/Python-SIP/sip/issues/12)
+
+### Determining the version of the limited API to use
+
+The version of the limited API to use is now taken from the `requires-python`
+field of the metadata in a project's `pyproject.toml` file. If this is not
+specified then (as with previous versions of SIP) the version of the oldest
+supported version of Python is used.
+
+Resolves [#58](https://github.com/Python-SIP/sip/issues/58)
+
+### `%Docstring` support for namespaces
+
+The `%Docstring` directive can now be specified for C++ namespaces.
+
+Resolves [#11](https://github.com/Python-SIP/sip/issues/11)
+
+### Support for `operator~()` in the global scope
+
+`operator~()` can now be specified in the global scope.
+
+Resolves [#9](https://github.com/Python-SIP/sip/issues/9)
+
+### Use consistent timestamps when creating wheel files
+
+The value of the environment variable `SOURCE_DATE_EPOCH`, if defined, will be
+used as the timestamp for all files included in a wheel. This ensures wheel
+building is repeatable.
+
+Pull request [#70](https://github.com/Python-SIP/sip/pull/70)
+
+### Bindings support for PEP 639
+
+- The `project` section of `pyproject.toml` files may now use `license` to
+ specify a valid SPDX license expression. `license-files`, if specified, is a
+ list of glob patterns describing the files containing licensing information.
+ The old style of `license` is deprecated.
+- The metadata format of the generated `PKG-INFO` file of an sdist will
+ normally be v2.4. If the deprecated form of `license` is used in
+ `pyproject.toml` then it will be v2.2.
+- License files will be installed in the `licenses` sub-directory of the
+ generated `.dist-info` directory of a wheel.
+- `packaging` v24.2 is now required.
+
+Resolves [#69](https://github.com/Python-SIP/sip/issues/69)
+
+### Normalised wheel names
+
+The names of wheel files (both those generated by `sip-wheel` and indirectly
+from the sdists created by `sip-module`) now conform to current PyPA standards.
+
+Resolves [#68](https://github.com/Python-SIP/sip/issues/68)
+
+### `pyproject.toml` now conforms to PEP 639
+
+The licensing information in SIP's `pyproject.toml` now conforms to PEP 639.
+This means that the minimum `setuptools` version is v77.
+
+### Bug fixes
+
+- The handling of unknown `%Timeline` tags in `%If` directives has been fixed.
+ An unknown tag is assumed to refer to a later version than all the known
+ tags. Therefore `(unknown -)` will always be false, and `(- unknown)` will
+ always be true.
+- Generated code will not contain digraphs. This usually affects C++
+ extensions being built with the default `setuptools` builder.
+- Long deprecation messages are now handled correctly. Pull request
+ [#67](https://github.com/Python-SIP/sip/pull/67)
+
+### Documentation fixes
+
+- Fixed the documentation for `SIP_ABI_MAJOR_VERSION` and
+ `SIP_ABI_MINOR_VERSION` for both v12 and v13 of the ABI.
+
+### Improved unit tests
+
+The support for writing unit tests for SIP has been improved and documented.
+In particular, test cases that require multiple test modules to be built are
+now supported.
+
+
## v6.10.0
### Introspection of the `sip` module ABI version
diff --git a/docs/specification_files.rst b/docs/specification_files.rst
index dce839a..5ccb542 100644
--- a/docs/specification_files.rst
+++ b/docs/specification_files.rst
@@ -215,7 +215,10 @@ file.
*namespace* ::= **namespace** *name* [**{** {*namespace-line*} **}**] **;**
- *namespace-line* ::= [:directive:`%TypeHeaderCode` | *statement*]
+ *namespace-line* ::= [
+ :directive:`%Docstring` |
+ :directive:`%TypeHeaderCode` |
+ *statement*]
*opaque-class* ::= **class** *scoped-name* **;**
diff --git a/pyproject.toml b/pyproject.toml
index ad22b4a..caeaf29 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
# The project configuration for sip.
[build-system]
-requires = ["setuptools>=64", "setuptools_scm>=8"]
+requires = ["setuptools>=77", "setuptools_scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
@@ -12,10 +12,10 @@ name = "sip"
description = "A Python bindings generator for C/C++ libraries"
readme = "README.md"
urls.homepage = "https://github.com/Python-SIP/sip"
-dependencies = ["packaging", "setuptools>=69.5", "tomli; python_version<'3.11'"]
+dependencies = ["packaging>=24.2", "setuptools>=75.8.1", "tomli; python_version<'3.11'"]
requires-python = ">=3.9"
-license = {file = "LICENSE"}
-classifiers = ["License :: OSI Approved :: BSD License"]
+license = "BSD-2-Clause"
+license-files = ["LICENSE"]
dynamic = ["version"]
[[project.authors]]
diff --git a/sipbuild/bindings.py b/sipbuild/bindings.py
index fe5ba68..e2c836a 100644
--- a/sipbuild/bindings.py
+++ b/sipbuild/bindings.py
@@ -151,7 +151,7 @@ class Bindings(Configurable):
project.sip_module)
# Update the target ABI for the project.
- if project.target_abi is None or project.target_abi < spec.target_abi:
+ if project.target_abi is None or project.target_abi[1] is None or project.target_abi < spec.target_abi:
project.target_abi = spec.target_abi
# Resolve the types.
diff --git a/sipbuild/buildable.py b/sipbuild/buildable.py
index 43b494a..dd8e324 100644
--- a/sipbuild/buildable.py
+++ b/sipbuild/buildable.py
@@ -61,8 +61,7 @@ class BuildableFromSources(Buildable):
if self.uses_limited_api:
self.define_macros.append(
- 'Py_LIMITED_API=0x03{0:02x}0000'.format(
- OLDEST_SUPPORTED_MINOR))
+ 'Py_LIMITED_API=' + project.limited_abi_version_str)
def make_names_relative(self):
""" Make all file and directory names relative to the build directory.
diff --git a/sipbuild/builder.py b/sipbuild/builder.py
index 612bcfc..171d057 100644
--- a/sipbuild/builder.py
+++ b/sipbuild/builder.py
@@ -48,8 +48,7 @@ class Builder(AbstractBuilder):
project = self.project
# The sdist name.
- sdist_name = '{}-{}'.format(project.name.replace('-', '_').lower(),
- project.version_str)
+ sdist_name = project.normalized_name
# Create the sdist root directory.
sdist_root = os.path.join(project.build_dir, sdist_name)
@@ -164,20 +163,25 @@ class Builder(AbstractBuilder):
# Copy the wheel contents.
self.install_project(wheel_build_dir, wheel_tag=wheel_tag)
- wheel_file = '{}-{}'.format(project.name.replace('-', '_'),
- project.version_str)
+ wheel_file = [project.normalized_name]
if project.build_tag:
- wheel_file += '-{}'.format(project.build_tag)
+ wheel_file.append(project.build_tag)
- wheel_file += '-{}.whl'.format(wheel_tag)
- wheel_path = os.path.abspath(os.path.join(wheel_directory, wheel_file))
+ wheel_file.append(wheel_tag)
+ wheel_path = os.path.abspath(
+ os.path.join(wheel_directory, '-'.join(wheel_file) + '.whl'))
# Create the .whl file.
saved_cwd = os.getcwd()
os.chdir(wheel_build_dir)
- from zipfile import ZipFile, ZIP_DEFLATED
+ import time
+ from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+
+ # Ensure reproducible wheel file timestamps
+ epoch = int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))
+ zip_timestamp = time.gmtime(epoch)[:6]
with ZipFile(wheel_path, 'w', compression=ZIP_DEFLATED) as zf:
for dirpath, _, filenames in os.walk('.'):
@@ -185,7 +189,10 @@ class Builder(AbstractBuilder):
# This will result in a name with no leading '.'.
name = os.path.relpath(os.path.join(dirpath, filename))
- zf.write(name)
+ zi = ZipInfo(name, zip_timestamp)
+
+ with open(name, 'rb') as f:
+ zf.writestr(zi, f.read())
os.chdir(saved_cwd)
diff --git a/sipbuild/distinfo/distinfo.py b/sipbuild/distinfo/distinfo.py
index f6ad7e8..84ff026 100644
--- a/sipbuild/distinfo/distinfo.py
+++ b/sipbuild/distinfo/distinfo.py
@@ -1,9 +1,10 @@
# SPDX-License-Identifier: BSD-2-Clause
-# Copyright (c) 2024 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
import base64
+import glob
import hashlib
import os
import shutil
@@ -89,6 +90,23 @@ def create_distinfo(distinfo_dir, wheel_tag, installed, metadata,
# Reproducable builds.
installed.sort()
+ # Copy any license 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)
+
+ os.chdir(saved)
+
if wheel_tag is None:
# Create the INSTALLER file.
installer_fn = os.path.join(distinfo_dir, 'INSTALLER')
diff --git a/sipbuild/generator/instantiations.py b/sipbuild/generator/instantiations.py
index f780053..b351a29 100644
--- a/sipbuild/generator/instantiations.py
+++ b/sipbuild/generator/instantiations.py
@@ -1,13 +1,14 @@
# 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 copy import copy
from .scoped_name import ScopedName
from .specification import (Argument, ArgumentType, FunctionCall,
- IfaceFileType, KwArgs, Signature, TypeHints, Value, ValueType)
+ IfaceFileType, KwArgs, Signature, Template, TypeHints, Value,
+ ValueType, WrappedClass)
from .templates import (template_code, template_code_blocks,
template_expansions, template_string)
from .utils import append_iface_file, cached_name, normalised_scoped_name
@@ -15,7 +16,7 @@ from .utils import append_iface_file, cached_name, normalised_scoped_name
def instantiate_class(p, symbol, fq_cpp_name, tmpl_names, proto_class,
template, py_name, no_type_name, docstring, pm):
- """ Instantiate a class template. """
+ """ Instantiate a class template and return the instantiated class. """
# Create the expansions.
expansions = template_expansions(tmpl_names, template.types)
@@ -75,28 +76,14 @@ def instantiate_class(p, symbol, fq_cpp_name, tmpl_names, proto_class,
i_class.superclasses = []
for superclass in proto_class.superclasses:
- superclass_name = superclass.iface_file.fq_cpp_name
-
- # Don't try and expand defined or scoped classes.
- if superclass.iface_file.module is not None or not superclass_name.is_simple:
- i_class.superclasses.append(superclass)
- continue
-
- for a, arg in enumerate(tmpl_names.args):
- if superclass_name.base_name == arg.definition.base_name:
- tmpl_arg = template.types.args[a]
-
- if tmpl_arg.type is ArgumentType.DEFINED:
- i_superclass = pm.find_class(p, symbol,
- IfaceFileType.CLASS, tmpl_arg.definition)
- elif tmpl_arg.type is ArgumentType.CLASS:
- i_superclass = tmpl_arg.definition
- else:
- pm.parser_error(p, symbol,
- "template argument '{0}' must expand to a class".format(superclass_name))
- i_superclass = superclass
+ if isinstance(superclass, WrappedClass):
+ klass = _superclass_from_class(superclass, p, symbol, tmpl_names,
+ template, pm)
+ else:
+ klass = _superclass_from_template(superclass, p, symbol,
+ tmpl_names, template, pm)
- i_class.superclasses.append(i_superclass)
+ i_class.superclasses.append(klass)
# Handle the enums.
_instantiate_enums(tmpl_names, proto_class, template, i_class, expansions,
@@ -153,6 +140,8 @@ def instantiate_class(p, symbol, fq_cpp_name, tmpl_names, proto_class,
pm.spec.classes.insert(0, i_class)
+ return i_class
+
def _instantiate_argument(proto_arg, proto_class, tmpl_names, template,
i_class, expansions, pm):
@@ -460,3 +449,116 @@ def _instantiate_vars(tmpl_names, proto_class, template, i_class, expansions,
expansions)
pm.spec.variables.append(i_var)
+
+
+def _superclass_from_class(klass, p, symbol, tmpl_names, template, pm):
+ """ Return the super-class based on a class or a template argument (that
+ resembles an undefined class.
+ """
+
+ superclass_name = klass.iface_file.fq_cpp_name
+
+ # Only deal with undefined classes with unscoped names which is how
+ # template argument names are passed.
+ if klass.iface_file.module is None and superclass_name.is_simple:
+ superclass = _find_argument_value(superclass_name, p, symbol,
+ tmpl_names, template, pm)
+
+ if superclass is None:
+ pm.parser_error(p, symbol,
+ "template argument '{0}' must expand to a class".format(
+ superclass_name))
+ else:
+ klass = superclass
+
+ return klass
+
+
+def _superclass_from_template(superclass, p, symbol, tmpl_names, template, pm):
+ """ Return the super-class based on a class template. """
+
+ # There are two approaches that could be taken with this. The first is to
+ # instantiate the super-class template to create a Python class (with an
+ # automatically generated name) to use as the super-class. The second is
+ # to add the contents of the class template to the original class being
+ # instantiated and so avoiding the creation of the 'internal' super-class
+ # class. We choose the first approach as it is closer to the existing
+ # implementation. The 'internal' super-class class is an undocumented (but
+ # visible) implementation detail.
+
+ # In order to instantiate the super-class template we need to create a new
+ # Template instance based on the super-class template invocation in the
+ # original class template but with the argument values replaced with those
+ # passed to the original class template.
+ proto_superclass_template = superclass.definition
+
+ superclass_template_args = []
+ superclass_name_parts = []
+
+ for proto_arg in proto_superclass_template.types.args:
+ # Assume we will use the argument as is.
+ superclass_arg = proto_arg
+
+ if proto_arg.type is ArgumentType.DEFINED and proto_arg.definition.is_simple:
+ klass = _find_argument_value(proto_arg.definition.base_name, p,
+ symbol, tmpl_names, template, pm)
+
+ if klass is not None:
+ superclass_arg = Argument(ArgumentType.CLASS, definition=klass)
+ superclass_name_parts.append(
+ klass.iface_file.fq_cpp_name.as_word)
+
+ superclass_template_args.append(superclass_arg)
+
+ superclass_template = Template(proto_superclass_template.cpp_name,
+ Signature(args=superclass_template_args))
+
+ # The generated C++ and Python names of the internal super-class is the
+ # name of the super-class template and the names of the values of each
+ # template argument.
+ fq_cpp_name = ScopedName(superclass_template.cpp_name)
+ fq_cpp_name[-1] = _make_generated_name(fq_cpp_name[-1],
+ superclass_name_parts)
+ py_name = _make_generated_name(superclass_template.cpp_name.as_py,
+ superclass_name_parts)
+
+ # Note that any error will have a misleading location as source_location
+ # should be used.
+ klass = pm.instantiate_class_template(p, symbol, fq_cpp_name,
+ superclass_template, py_name, no_type_name=True, docstring=None)
+
+ if klass is None:
+ pm.parser_error_at_location(superclass.source_location,
+ "unknown class template")
+
+ return klass
+
+
+def _find_argument_value(name, p, symbol, tmpl_names, template, pm):
+ """ Return the WrappedClass instance that is the value of a named argument.
+ None is returned is an appropriate value couldn't be found.
+ """
+
+ for a, arg in enumerate(tmpl_names.args):
+ if arg.definition.base_name == name:
+ tmpl_arg = template.types.args[a]
+
+ if tmpl_arg.type is ArgumentType.DEFINED:
+ return pm.find_class(p, symbol, IfaceFileType.CLASS,
+ tmpl_arg.definition)
+
+ if tmpl_arg.type is ArgumentType.CLASS:
+ return tmpl_arg.definition
+
+ return None
+
+
+def _make_generated_name(base_name, name_parts):
+ """ Return the full generated name from a base name and parts representing
+ each template argument value.
+ """
+
+ all_parts = [base_name]
+ all_parts.extend(name_parts)
+
+ return '__'.join(all_parts)
diff --git a/sipbuild/generator/outputs/code/code.py b/sipbuild/generator/outputs/code/code.py
index 997e3c3..bc84e07 100644
--- a/sipbuild/generator/outputs/code/code.py
+++ b/sipbuild/generator/outputs/code/code.py
@@ -1163,17 +1163,18 @@ f''' /* Export the module and publish it's API. */
# Import the helpers.
sf.write(
f'''
+
sip_{module_name}_qt_metaobject = (sip_qt_metaobject_func)sipImportSymbol("qtcore_qt_metaobject");
sip_{module_name}_qt_metacall = (sip_qt_metacall_func)sipImportSymbol("qtcore_qt_metacall");
sip_{module_name}_qt_metacast = (sip_qt_metacast_func)sipImportSymbol("qtcore_qt_metacast");
if (!sip_{module_name}_qt_metacast)
Py_FatalError("Unable to import qtcore_qt_metacast");
-
''')
sf.write(
-f''' /* Initialise the module now all its dependencies have been set up. */
+f'''
+ /* Initialise the module now all its dependencies have been set up. */
if (sipInitModule(&sipModuleAPI_{module_name}, sipModuleDict) < 0)
{{
Py_DECREF(sipModule);
@@ -5353,6 +5354,9 @@ def _call_args(sf, spec, cpp_signature, py_signature):
indirection = ''
nr_derefs = len(arg.derefs)
+ # The argument may be surrounded by something type-specific.
+ prefix = suffix = ''
+
if arg.type in (ArgumentType.ASCII_STRING, ArgumentType.LATIN1_STRING, ArgumentType.UTF8_STRING, ArgumentType.SSTRING, ArgumentType.USTRING, ArgumentType.STRING, ArgumentType.WSTRING):
if nr_derefs > (0 if arg.is_out else 1) and not arg.is_reference:
indirection = '&'
@@ -5363,6 +5367,10 @@ def _call_args(sf, spec, cpp_signature, py_signature):
elif nr_derefs == 0:
indirection = '*'
+ if arg.type is ArgumentType.MAPPED and arg.definition.movable:
+ prefix = 'std::move('
+ suffix = ')'
+
elif arg.type in (ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.VOID):
if nr_derefs == 2:
indirection = '&'
@@ -5394,12 +5402,12 @@ def _call_args(sf, spec, cpp_signature, py_signature):
else:
sf.write(f'reinterpret_cast<{arg_cpp_type_name} *>({arg_name})')
else:
- sf.write(indirection)
+ sf.write(prefix + indirection)
if arg.array is ArrayArgument.ARRAY_SIZE:
sf.write(f'({arg_cpp_type_name})')
- sf.write(arg_name)
+ sf.write(arg_name + suffix)
def _get_named_value_decl(spec, scope, type, name):
@@ -9126,7 +9134,11 @@ class SourceFile:
def write(self, s):
""" Write a string while tracking the current line number. """
- self._f.write(s)
+ # Older C++ standards (pre-C++17) get confused with digraphs (usually
+ # when the default setuptools is being used to build C++ extensions).
+ # The easiest solution is to hack the string for the most common case
+ # and hope it doesn't have unintended consequences.
+ self._f.write(s.replace('_cast<::', '_cast< ::'))
self._line_nr += s.count('\n')
def write_code(self, code):
diff --git a/sipbuild/generator/outputs/formatters/template.py b/sipbuild/generator/outputs/formatters/template.py
index 5a72f18..258b414 100644
--- a/sipbuild/generator/outputs/formatters/template.py
+++ b/sipbuild/generator/outputs/formatters/template.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 ...scoped_name import STRIP_NONE
@@ -14,4 +14,8 @@ def fmt_template_as_cpp_type(spec, template, strip=STRIP_NONE, as_xml=False):
sig_s = fmt_signature_as_cpp_declaration(spec, template.types, strip=strip,
as_xml=as_xml)
+ # Handle digraphs.
+ if sig_s.startswith('::'):
+ sig_s = ' ' + sig_s
+
return template.cpp_name.cpp_stripped(strip) + '<' + sig_s + '>'
diff --git a/sipbuild/generator/parser/annotations.py b/sipbuild/generator/parser/annotations.py
index d04a0af..2681577 100644
--- a/sipbuild/generator/parser/annotations.py
+++ b/sipbuild/generator/parser/annotations.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>
class DottedName(str):
@@ -229,6 +229,7 @@ _ANNOTATION_TYPES = {
'KeywordArgs': string(),
'Metatype': name(allow_dots=True),
'Mixin': boolean(),
+ 'Movable': boolean(),
'NewThread': boolean(),
'NoArgParser': boolean(),
'NoAssignmentOperator': boolean(),
diff --git a/sipbuild/generator/parser/parser_manager.py b/sipbuild/generator/parser/parser_manager.py
index 0f1d771..9757369 100644
--- a/sipbuild/generator/parser/parser_manager.py
+++ b/sipbuild/generator/parser/parser_manager.py
@@ -52,7 +52,7 @@ class ParserManager:
rules.parser = self._parser
# The list of class templates. Each element is a 2-tuple of the
- # template arguments and the class itself.
+ # template arguments (as a Signature instance) and the class itself.
self.class_templates = []
# Public state.
@@ -256,7 +256,7 @@ class ParserManager:
type_hints=self.get_type_hints(p, symbol, annotations))
klass.class_key = class_key
- klass.superclasses = superclasses
+ klass.superclasses = superclasses if superclasses is not None else []
self.push_scope(klass,
AccessSpecifier.PRIVATE if class_key is ClassKey.CLASS else AccessSpecifier.PUBLIC)
@@ -579,7 +579,9 @@ class ParserManager:
base_type = EnumBaseType.ENUM
if base_type_s is not None:
- if self.spec.target_abi is not None and self.spec.target_abi < (13, 0):
+ # 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:
self.parser_error(p, symbol,
"/BaseType/ is only supported for ABI v13.0 and later")
@@ -917,6 +919,7 @@ class ParserManager:
""" Apply annotations to a mapped type. """
mapped_type.handles_none = annotations.get('AllowNone', False)
+ mapped_type.movable = annotations.get('Movable', False)
mapped_type.no_assignment_operator = annotations.get(
'NoAssignmentOperator', False)
mapped_type.no_copy_ctor = annotations.get('NoCopyCtor', False)
@@ -1244,7 +1247,7 @@ class ParserManager:
if upper_name:
upper_qual = self._find_timeline_qualifier(p, symbol_upper)
if upper_qual is None:
- return False
+ return True
else:
upper_qual = None
@@ -1446,8 +1449,8 @@ class ParserManager:
def instantiate_class_template(self, p, symbol, fq_cpp_name, template,
py_name, no_type_name, docstring):
- """ Try and instantiate a class template and return True if one was
- found.
+ """ Try and instantiate a class template and return the instantiated
+ class or None if no template was found.
"""
# Look for an appropriate class template.
@@ -1456,12 +1459,10 @@ class ParserManager:
break
else:
# There was no class template to instantiate.
- return False
-
- instantiate_class(p, symbol, fq_cpp_name, tmpl_names, proto_class,
- template, py_name, no_type_name, docstring, self)
+ return None
- return True
+ return instantiate_class(p, symbol, fq_cpp_name, tmpl_names,
+ proto_class, template, py_name, no_type_name, docstring, self)
def lexer_error(self, t, text):
""" Record an error caused by a token. """
@@ -1531,7 +1532,13 @@ class ParserManager:
def parser_error(self, p, symbol, text):
""" Record an error caused by a symbol in a production. """
- self._error_log.log(text, self.get_source_location(p, symbol))
+ self.parser_error_at_location(self.get_source_location(p, symbol),
+ text)
+
+ def parser_error_at_location(self, source_location, text):
+ """ Record an error caused by a symbol in a production. """
+
+ self._error_log.log(text, source_location)
def pop_file(self):
""" Restore the current .sip file from the stack and make it current.
@@ -1997,7 +2004,7 @@ class ParserManager:
name = p[symbol]
- qual = self.find_qualifier(p, symbol, name)
+ qual = self.find_qualifier(p, symbol, name, required=False)
if qual is None:
return None
diff --git a/sipbuild/generator/parser/rules.py b/sipbuild/generator/parser/rules.py
index 85090e1..6fd99b7 100644
--- a/sipbuild/generator/parser/rules.py
+++ b/sipbuild/generator/parser/rules.py
@@ -93,6 +93,7 @@ def p_statement(p):
def p_namespace_statement(p):
"""namespace_statement : if_start
| if_end
+ | namespace_docstring
| class_decl
| class_template
| enum_decl
@@ -869,6 +870,7 @@ def p_license_arg(p):
# The mapped type annotations.
_MAPPED_TYPE_ANNOTATIONS = (
'AllowNone',
+ 'Movable',
'NoAssignmentOperator',
'NoCopyCtor',
'NoDefaultCtor',
@@ -1872,7 +1874,7 @@ def p_superclass_list(p):
def p_superclass(p):
- "superclass : class_access scoped_name"
+ "superclass : class_access simple_superclass"
pm = p.parser.pm
@@ -1883,21 +1885,7 @@ def p_superclass(p):
p[0] = None
return
- # This is a hack to allow typedef'ed classes to be used before we have
- # resolved the typedef definitions. Unlike elsewhere, we require that the
- # typedef is defined before being used.
- ad = Argument(ArgumentType.DEFINED, definition=p[2])
-
- while ad.type is ArgumentType.DEFINED:
- ad.type = ArgumentType.NONE
- search_typedefs(pm.spec, ad.definition, ad)
-
- if ad.type is not ArgumentType.NONE or len(ad.derefs) != 0 or ad.is_const or ad.is_reference:
- pm.parser_error(p, 2, "super-class list contains an invalid type")
-
- # Find the actual class.
- p[0] = pm.find_class(p, 2, IfaceFileType.CLASS, ad.definition,
- tmpl_arg=pm.parsing_template)
+ p[0] = p[2]
def p_class_access(p):
@@ -1912,6 +1900,43 @@ def p_class_access(p):
p[0] = p[1]
+def p_simple_superclass(p):
+ """simple_superclass : scoped_name
+ | scoped_name '<' cpp_types '>'"""
+
+ pm = p.parser.pm
+
+ # Handle templates.
+ if len(p) == 5:
+ if pm.parsing_template:
+ # For the moment we just remember enough to identify it later when
+ # we have the values to instantiate it.
+ p[0] = Argument(ArgumentType.TEMPLATE,
+ definition=Template(p[1], Signature(args=p[3])),
+ source_location=pm.get_source_location(p, 1))
+ else:
+ pm.parser_error(p, 1,
+ "super-class templates can only be used in a class template")
+
+ return
+
+ # This is a hack to allow typedef'ed classes to be used before we have
+ # resolved the typedef definitions. Unlike elsewhere, we require that the
+ # typedef is defined before being used.
+ ad = Argument(ArgumentType.DEFINED, definition=p[1])
+
+ while ad.type is ArgumentType.DEFINED:
+ ad.type = ArgumentType.NONE
+ search_typedefs(pm.spec, ad.definition, ad)
+
+ if ad.type is not ArgumentType.NONE or len(ad.derefs) != 0 or ad.is_const or ad.is_reference:
+ pm.parser_error(p, 1, "super-class list contains an invalid type")
+
+ # Find the actual class.
+ p[0] = pm.find_class(p, 1, IfaceFileType.CLASS, ad.definition,
+ tmpl_arg=pm.parsing_template)
+
+
def p_opt_class_definition(p):
"""opt_class_definition : '{' opt_class_body '}'
| empty"""
@@ -3014,6 +3039,20 @@ def p_namespace_body(p):
"""namespace_body : namespace_statement
| namespace_body namespace_statement"""
+def p_namespace_docstring(p):
+ "namespace_docstring : docstring"
+
+ pm = p.parser.pm
+
+ if pm.skipping:
+ return
+
+ if pm.scope.docstring is None:
+ pm.scope.docstring = p[1]
+ else:
+ pm.parser_error(p, 1,
+ "%Docstring has already been defined for this namespace")
+
# C/C++ typedefs. #############################################################
diff --git a/sipbuild/generator/python_slots.py b/sipbuild/generator/python_slots.py
index 893fe7d..097544c 100644
--- a/sipbuild/generator/python_slots.py
+++ b/sipbuild/generator/python_slots.py
@@ -80,7 +80,7 @@ def invalid_global_slot(slot):
level) slot.
"""
- if slot in (PySlot.NEG, PySlot.POS):
+ if slot in (PySlot.INVERT, PySlot.NEG, PySlot.POS):
return False
if is_number_slot(slot):
diff --git a/sipbuild/generator/resolver/resolver.py b/sipbuild/generator/resolver/resolver.py
index e4211f0..7921a5e 100644
--- a/sipbuild/generator/resolver/resolver.py
+++ b/sipbuild/generator/resolver/resolver.py
@@ -1345,6 +1345,11 @@ def _resolve_py_signature_types(spec, mod, scope, overload, error_log,
lambda: _check_array_support(overload, local_arg_nr, scope,
error_log))
+ # If the type of the argument is a /Movable/ mapped type then add
+ # /Transfer/.
+ if arg.type is ArgumentType.MAPPED and arg.definition.movable:
+ arg.transfer = Transfer.TRANSFER
+
if scope is not None:
_scope_default_value(spec, scope, arg)
@@ -1833,6 +1838,7 @@ def _instantiate_mapped_type_template(spec, mod, mapped_type_template, type,
proto_mapped_type = mapped_type_template.mapped_type
mapped_type.handles_none = proto_mapped_type.handles_none
+ mapped_type.movable = proto_mapped_type.movable
mapped_type.needs_user_state = proto_mapped_type.needs_user_state
mapped_type.no_assignment_operator = proto_mapped_type.no_assignment_operator
mapped_type.no_copy_ctor = proto_mapped_type.no_copy_ctor
diff --git a/sipbuild/generator/specification.py b/sipbuild/generator/specification.py
index 57376ba..79c9e3d 100644
--- a/sipbuild/generator/specification.py
+++ b/sipbuild/generator/specification.py
@@ -25,8 +25,8 @@ class AccessSpecifier(Enum):
class ArgumentType(Enum):
""" The types of either C/C++ or Python arguments. The numerical values of
- these can occur in generated code so so types must always be appended and
- old (unused) types must never be removed.
+ these can occur in generated code so types must always be appended and old
+ (unused) types must never be removed.
"""
# The type hasn't been specified.
@@ -847,6 +847,9 @@ class MappedType:
# The member functions.
members: list['Member'] = field(default_factory=list)
+ # Set if /Movable/ was specified.
+ movable: bool = False
+
# Set if the handwritten code requires user state information.
needs_user_state: bool = False
@@ -1620,8 +1623,9 @@ class WrappedClass:
# The sub-class base class. (resolver)
subclass_base: Optional['WrappedClass'] = None
- # The super-classes.
- superclasses: list['WrappedClass'] = field(default_factory=list)
+ # The super-classes. A super-class can only be a template argument in a
+ # class template.
+ superclasses: list[Union[Argument, 'WrappedClass']] = field(default_factory=list)
# The value of /Supertype/ if specified.
supertype: Optional[CachedName] = None
diff --git a/sipbuild/generator/utils.py b/sipbuild/generator/utils.py
index a75308e..1b746c4 100644
--- a/sipbuild/generator/utils.py
+++ b/sipbuild/generator/utils.py
@@ -44,7 +44,7 @@ def argument_as_str(arg):
s += '>'
- elif arg.type in (ArgumentType.STRUCT, ArgumentType.DEFINED):
+ elif arg.type in (ArgumentType.CLASS, ArgumentType.STRUCT, ArgumentType.UNION, ArgumentType.DEFINED):
s = str(arg.definition)
elif arg.type in (ArgumentType.UBYTE, ArgumentType.USTRING):
diff --git a/sipbuild/module/source/12/16/sip.h.in b/sipbuild/module/source/12/16/sip.h.in
index 8f33579..9fffb50 100644
--- a/sipbuild/module/source/12/16/sip.h.in
+++ b/sipbuild/module/source/12/16/sip.h.in
@@ -36,7 +36,7 @@ extern "C" {
/* The version of the ABI. */
#define SIP_ABI_MAJOR_VERSION 12
#define SIP_ABI_MINOR_VERSION 16
-#define SIP_MODULE_PATCH_VERSION 1
+#define SIP_MODULE_PATCH_VERSION 2
/*
diff --git a/sipbuild/module/source/12/16/siplib.c b/sipbuild/module/source/12/16/siplib.c
index 409bba8..9b7dc6e 100644
--- a/sipbuild/module/source/12/16/siplib.c
+++ b/sipbuild/module/source/12/16/siplib.c
@@ -7792,21 +7792,25 @@ int sip_api_deprecated(const char *classname, const char *method)
int sip_api_deprecated_12_16(const char *classname, const char *method,
const char *message)
{
- char buf[100];
+ const unsigned int bufsize = 100 + ( message ? strlen(message) : 0 );
+ char *buf = (char*)sip_api_malloc(bufsize * sizeof(char));
+ unsigned int written = 0;
if (classname == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s() is deprecated", method);
+ written = PyOS_snprintf(buf, bufsize, "%s() is deprecated", method);
else if (method == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s constructor is deprecated",
- classname);
+ written = PyOS_snprintf(buf, bufsize, "%s constructor is deprecated",
+ classname);
else
- PyOS_snprintf(buf, sizeof (buf), "%s.%s() is deprecated", classname,
- method);
+ written = PyOS_snprintf(buf, bufsize, "%s.%s() is deprecated", classname,
+ method );
if (message != NULL)
- PyOS_snprintf(&buf[strlen(buf)], sizeof (buf), ": %s", message);
+ PyOS_snprintf(buf+written, bufsize-written, ": %s", message);
- return PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ const int res = PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ sip_api_free(buf);
+ return res;
}
diff --git a/sipbuild/module/source/12/17/sip.h.in b/sipbuild/module/source/12/17/sip.h.in
index 9c13aae..6289f7e 100644
--- a/sipbuild/module/source/12/17/sip.h.in
+++ b/sipbuild/module/source/12/17/sip.h.in
@@ -36,7 +36,7 @@ extern "C" {
/* The version of the ABI. */
#define SIP_ABI_MAJOR_VERSION 12
#define SIP_ABI_MINOR_VERSION 17
-#define SIP_MODULE_PATCH_VERSION 0
+#define SIP_MODULE_PATCH_VERSION 1
/*
diff --git a/sipbuild/module/source/12/17/siplib.c b/sipbuild/module/source/12/17/siplib.c
index a9fb64b..a65e399 100644
--- a/sipbuild/module/source/12/17/siplib.c
+++ b/sipbuild/module/source/12/17/siplib.c
@@ -7800,21 +7800,25 @@ int sip_api_deprecated(const char *classname, const char *method)
int sip_api_deprecated_12_16(const char *classname, const char *method,
const char *message)
{
- char buf[100];
+ const unsigned int bufsize = 100 + ( message ? strlen(message) : 0 );
+ char *buf = (char*)sip_api_malloc(bufsize * sizeof(char));
+ unsigned int written = 0;
if (classname == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s() is deprecated", method);
+ written = PyOS_snprintf(buf, bufsize, "%s() is deprecated", method);
else if (method == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s constructor is deprecated",
- classname);
+ written = PyOS_snprintf(buf, bufsize, "%s constructor is deprecated",
+ classname);
else
- PyOS_snprintf(buf, sizeof (buf), "%s.%s() is deprecated", classname,
- method);
+ written = PyOS_snprintf(buf, bufsize, "%s.%s() is deprecated", classname,
+ method );
if (message != NULL)
- PyOS_snprintf(&buf[strlen(buf)], sizeof (buf), ": %s", message);
+ PyOS_snprintf(buf+written, bufsize-written, ": %s", message);
- return PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ const int res = PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ sip_api_free(buf);
+ return res;
}
@@ -9825,7 +9829,7 @@ static PyObject *sipWrapperType_alloc(PyTypeObject *self, Py_ssize_t nitems)
((sipWrapperType *)o)->wt_td = currentType;
- if (sipTypeIsClass(currentType))
+ if (sipTypeIsClass(currentType) || sipTypeIsNamespace(currentType))
{
const sipClassTypeDef *ctd = (const sipClassTypeDef *)currentType;
const char *docstring = ctd->ctd_docstring;
diff --git a/sipbuild/module/source/13/10/sip.h.in b/sipbuild/module/source/13/10/sip.h.in
index ce0acc1..75f8f52 100644
--- a/sipbuild/module/source/13/10/sip.h.in
+++ b/sipbuild/module/source/13/10/sip.h.in
@@ -36,7 +36,7 @@ extern "C" {
/* The version of the ABI. */
#define SIP_ABI_MAJOR_VERSION 13
#define SIP_ABI_MINOR_VERSION 10
-#define SIP_MODULE_PATCH_VERSION 0
+#define SIP_MODULE_PATCH_VERSION 1
/*
diff --git a/sipbuild/module/source/13/10/sip_core.c b/sipbuild/module/source/13/10/sip_core.c
index 54e89b1..ba72f7c 100644
--- a/sipbuild/module/source/13/10/sip_core.c
+++ b/sipbuild/module/source/13/10/sip_core.c
@@ -6685,21 +6685,25 @@ int sip_api_deprecated(const char *classname, const char *method)
int sip_api_deprecated_13_9(const char *classname, const char *method,
const char *message)
{
- char buf[100];
+ const unsigned int bufsize = 100 + ( message ? strlen(message) : 0 );
+ char *buf = (char*)sip_api_malloc(bufsize * sizeof(char));
+ unsigned int written = 0;
if (classname == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s() is deprecated", method);
+ written = PyOS_snprintf(buf, bufsize, "%s() is deprecated", method);
else if (method == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s constructor is deprecated",
- classname);
+ written = PyOS_snprintf(buf, bufsize, "%s constructor is deprecated",
+ classname);
else
- PyOS_snprintf(buf, sizeof (buf), "%s.%s() is deprecated", classname,
- method );
+ written = PyOS_snprintf(buf, bufsize, "%s.%s() is deprecated", classname,
+ method );
if (message != NULL)
- PyOS_snprintf(&buf[strlen(buf)], sizeof (buf), ": %s", message);
+ PyOS_snprintf(buf+written, bufsize-written, ": %s", message);
- return PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ const int res = PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ sip_api_free(buf);
+ return res;
}
@@ -8610,7 +8614,7 @@ static PyObject *sipWrapperType_alloc(PyTypeObject *self, Py_ssize_t nitems)
((sipWrapperType *)o)->wt_td = currentType;
- if (sipTypeIsClass(currentType))
+ if (sipTypeIsClass(currentType) || sipTypeIsNamespace(currentType))
{
const sipClassTypeDef *ctd = (const sipClassTypeDef *)currentType;
const char *docstring = ctd->ctd_docstring;
diff --git a/sipbuild/module/source/13/9/sip.h.in b/sipbuild/module/source/13/9/sip.h.in
index dd452d9..fd0835a 100644
--- a/sipbuild/module/source/13/9/sip.h.in
+++ b/sipbuild/module/source/13/9/sip.h.in
@@ -36,7 +36,7 @@ extern "C" {
/* The version of the ABI. */
#define SIP_ABI_MAJOR_VERSION 13
#define SIP_ABI_MINOR_VERSION 9
-#define SIP_MODULE_PATCH_VERSION 1
+#define SIP_MODULE_PATCH_VERSION 2
/*
diff --git a/sipbuild/module/source/13/9/sip_core.c b/sipbuild/module/source/13/9/sip_core.c
index f29485c..881c422 100644
--- a/sipbuild/module/source/13/9/sip_core.c
+++ b/sipbuild/module/source/13/9/sip_core.c
@@ -6677,21 +6677,25 @@ int sip_api_deprecated(const char *classname, const char *method)
int sip_api_deprecated_13_9(const char *classname, const char *method,
const char *message)
{
- char buf[100];
+ const unsigned int bufsize = 100 + ( message ? strlen(message) : 0 );
+ char *buf = (char*)sip_api_malloc(bufsize * sizeof(char));
+ unsigned int written = 0;
if (classname == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s() is deprecated", method);
+ written = PyOS_snprintf(buf, bufsize, "%s() is deprecated", method);
else if (method == NULL)
- PyOS_snprintf(buf, sizeof (buf), "%s constructor is deprecated",
- classname);
+ written = PyOS_snprintf(buf, bufsize, "%s constructor is deprecated",
+ classname);
else
- PyOS_snprintf(buf, sizeof (buf), "%s.%s() is deprecated", classname,
- method );
+ written = PyOS_snprintf(buf, bufsize, "%s.%s() is deprecated", classname,
+ method );
if (message != NULL)
- PyOS_snprintf(&buf[strlen(buf)], sizeof (buf), ": %s", message);
+ PyOS_snprintf(buf+written, bufsize-written, ": %s", message);
- return PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ const int res = PyErr_WarnEx(PyExc_DeprecationWarning, buf, 1);
+ sip_api_free(buf);
+ return res;
}
diff --git a/sipbuild/project.py b/sipbuild/project.py
index 14b9bdb..faf6744 100644
--- a/sipbuild/project.py
+++ b/sipbuild/project.py
@@ -295,9 +295,7 @@ class Project(AbstractProject, Configurable):
""" Return the name of the .dist-info directory for a target directory.
"""
- return os.path.join(target_dir,
- '{}-{}.dist-info'.format(self.name.replace('-', '_'),
- self.version_str))
+ return os.path.join(target_dir, self.normalized_name + '.dist-info')
def get_dunder_init(self):
""" Return the contents of the __init__.py to install. """
@@ -334,7 +332,7 @@ class Project(AbstractProject, Configurable):
name_parts = self.sip_module.split('.')
del name_parts[-1]
- return os.path.join(*name_parts)
+ return os.path.join(*name_parts) if name_parts else '.'
return ''
@@ -541,6 +539,12 @@ class Project(AbstractProject, Configurable):
self._minimum_macos_version = value
+ @property
+ def normalized_name(self):
+ """ The normalized project name including the version number. """
+
+ return self.name.replace('-', '_').lower() + '-' + self.version_str
+
@staticmethod
def open_for_writing(fname):
""" Open a file for writing while handling any errors. """
@@ -844,28 +848,22 @@ class Project(AbstractProject, Configurable):
self.metadata = pyproject.get_metadata()
self._metadata_overrides = self.get_metadata_overrides()
self.metadata.update(self._metadata_overrides)
- self.version_str = self.metadata['version']
-
- # Convert the version as a string to number.
- base_version = packaging.version.parse(self.version_str).base_version
- base_version = base_version.split('.')
- while len(base_version) < 3:
- base_version.append('0')
+ # Get the version string and convert it to a number.
+ self.version_str = self.metadata['version']
- version = 0
- for part in base_version:
- version <<= 8
+ try:
+ version = packaging.version.parse(self.version_str)
+ except:
+ raise PyProjectOptionException('version',
+ "'{0}' is an invalid version number".format(
+ self.version_str),
+ section_name='tool.sip.metadata')
- try:
- version += int(part)
- except ValueError:
- raise PyProjectOptionException('version',
- "'{0}' is an invalid version number".format(
- self.version_str),
- section_name='tool.sip.metadata')
+ self.version = version.major << 16 | version.minor << 8 | version.micro
- self.version = version
+ # Get the limited ABI version string.
+ self.limited_abi_version_str = self._get_limited_abi_version_str()
# Configure the project.
self.configure(pyproject, 'tool.sip.project', tool)
@@ -892,3 +890,28 @@ class Project(AbstractProject, Configurable):
for bindings in self.bindings.values():
bindings.configure(pyproject, 'tool.sip.bindings.' + bindings.name,
tool)
+
+ def _get_limited_abi_version_str(self):
+ """ Get the version of the limited ABI to be used. """
+
+ try:
+ # The version of the ABI to use is taken from the project metadata.
+ spec_set = packaging.specifiers.SpecifierSet(
+ self.metadata['requires-python'])
+
+ # Find the oldest Python version that satisfies the requirement.
+ # The 100 is an arbitrary upper bound.
+ min_req_version = next(
+ spec_set.filter((f'3.{v}' for v in range(100))))
+
+ min_req_version = packaging.version.parse(min_req_version)
+ limited_abi_version_str = '0x{0:02x}{1:02x}{2:02x}00'.format(
+ min_req_version.major,
+ min_req_version.minor,
+ min_req_version.micro)
+ except Exception as e:
+ # Default to the oldest version of Python we support.
+ limited_abi_version_str = '0x03{0:02x}0000'.format(
+ OLDEST_SUPPORTED_MINOR)
+
+ return limited_abi_version_str
diff --git a/sipbuild/py_versions.py b/sipbuild/py_versions.py
index 5e7f325..c52a101 100644
--- a/sipbuild/py_versions.py
+++ b/sipbuild/py_versions.py
@@ -1,12 +1,12 @@
# SPDX-License-Identifier: BSD-2-Clause
-# Copyright (c) 2024 Phil Thompson <phil at riverbankcomputing.com>
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
# The minimum required version of setuptools. This is the earliest version
-# that supports PEP 625. Remember to update pyproject.toml in the root
-# directory.
-MINIMUM_SETUPTOOLS = '69.5'
+# that generates correct wheel names for PyPI. Remember to update
+# pyproject.toml in the root directory.
+MINIMUM_SETUPTOOLS = '75.8.1'
# The oldest supported minor version of Python v3. Remember to update
# pyproject.toml in the root directory.
diff --git a/sipbuild/pyproject.py b/sipbuild/pyproject.py
index c49051f..791ba72 100644
--- a/sipbuild/pyproject.py
+++ b/sipbuild/pyproject.py
@@ -3,6 +3,9 @@
# Copyright (c) 2024 Phil Thompson <phil at riverbankcomputing.com>
+from packaging.licenses import (canonicalize_license_expression,
+ InvalidLicenseExpression)
+
from .exceptions import deprecated, UserFileException
from .py_versions import OLDEST_SUPPORTED_MINOR
from .toml import toml_loads
@@ -99,7 +102,7 @@ class PyProject:
else:
# The main validation we do is to check that the keys are defined
# by PEP 621.
- core_metadata = {'metadata-version': '2.1'}
+ core_metadata = {'metadata-version': '2.4'}
for md_name, md_value in metadata.items():
md_name = md_name.lower()
@@ -109,6 +112,8 @@ class PyProject:
self._handle_readme(md_value, core_metadata)
elif md_name == 'license':
self._handle_license(md_value, core_metadata)
+ elif md_name == 'license-files':
+ self._handle_license_files(md_value, core_metadata)
elif md_name == 'authors':
self._handle_people(md_value, 'author', core_metadata)
elif md_name == 'maintainers':
@@ -134,6 +139,21 @@ class PyProject:
core_metadata[core_name] = md_value
+ # Check the legacy (ie. pre PEP 639) use of 'license'.
+ if 'license' in core_metadata:
+ if 'license-file' in core_metadata:
+ raise PyProjectOptionException('license',
+ "cannot be used in it's legacy form if 'license-files' is also specified",
+ section_name='project')
+
+ deprecated(
+ "the legacy use of 'license'",
+ instead="an SPDX license expression and 'license-files'",
+ filename='pyproject.toml',
+ line_nr=self._get_line_nr('license ='))
+
+ core_metadata['metadata-version'] = '2.2'
+
# Check that the name has been specified.
if 'name' not in core_metadata:
raise PyProjectUndefinedOptionException('name',
@@ -229,8 +249,8 @@ class PyProject:
section_name='tool.sip')
if core_metadata_version is None:
- # Default to PEP 566.
- core_metadata['metadata-version'] = '2.1'
+ # Default to PEP 643.
+ core_metadata['metadata-version'] = '2.2'
return core_metadata
@@ -272,13 +292,22 @@ class PyProject:
core_metadata['keywords'] = ', '.join(value)
- @staticmethod
- def _handle_license(value, core_metadata):
+ @classmethod
+ def _handle_license(cls, value, core_metadata):
""" Handle the 'license' key. """
- if not isinstance(value, dict):
- raise PyProjectTypeOptionException('license', "a table",
- section_name='project')
+ if isinstance(value, dict):
+ return cls._handle_legacy_license(value, core_metadata)
+
+ try:
+ core_metadata['license-expression'] = canonicalize_license_expression(value)
+ except InvalidLicenseExpression as e:
+ raise PyProjectTypeOptionException('license',
+ "a valid SPDX license expression", section_name='project')
+
+ @staticmethod
+ def _handle_legacy_license(value, core_metadata):
+ """ Handle the legacy 'license' key. """
text = value.get('text')
file = value.get('file')
@@ -303,6 +332,16 @@ class PyProject:
core_metadata['license'] = text
+ @staticmethod
+ def _handle_license_files(value, core_metadata):
+ """ Handle the 'license-files' key. """
+
+ if not isinstance(value, list):
+ raise PyProjectTypeOptionException('license-files',
+ "an array of strings", section_name='project')
+
+ core_metadata['license-file'] = value
+
@classmethod
def _handle_optional_dependencies(cls, value, core_metadata):
""" Handle the 'optional-dependencies' key. """
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..09ccf41
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,31 @@
+# Unit Tests
+
+Unit tests are written using Python's `unittest` module. Each sub-directory
+implements a single test case, each of which may contain any number of tests.
+
+
+## Running Unit Tests
+
+To run the complete suite of tests, run:
+
+ python -m unittest
+
+To run the tests of a particular test case, run:
+
+ python -m unittest TEST_SUBDIR/test_TEST.py
+
+
+## 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.
+
+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
+the `.sip` files) needed to run the tests. The modules will be available as
+top-level imports. A module's build is configurable to a limited extent - see
+the class attributes of `SIPTestCase` for the relevant information.
+
+Each `.sip` file should define a single module to build. The C/C++
+implementation being wrapped would normally be embedded in the `.sip` file
+using appropriate code directives.
diff --git a/test/imported_exceptions/__init__.py b/test/imported_exceptions/__init__.py
new file mode 100644
index 0000000..6f186ca
--- /dev/null
+++ b/test/imported_exceptions/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
diff --git a/test/imported_exceptions/handler_module.sip b/test/imported_exceptions/handler_module.sip
new file mode 100644
index 0000000..04c1f30
--- /dev/null
+++ b/test/imported_exceptions/handler_module.sip
@@ -0,0 +1,19 @@
+// The SIP implementation of the handler_module test module.
+
+
+%Module(name=handler_module)
+
+
+%Exception std::exception(SIP_Exception) /PyName=StdException, Default/
+{
+%TypeHeaderCode
+#include <exception>
+%End
+
+%RaiseCode
+ const char *detail = sipExceptionRef.what();
+ SIP_BLOCK_THREADS
+ PyErr_SetString(sipException_std_exception, detail);
+ SIP_UNBLOCK_THREADS
+%End
+};
diff --git a/test/imported_exceptions/test_imported_exceptions.py b/test/imported_exceptions/test_imported_exceptions.py
new file mode 100644
index 0000000..b029553
--- /dev/null
+++ b/test/imported_exceptions/test_imported_exceptions.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPTestCase
+
+
+class ImportedExceptionsTestCase(SIPTestCase):
+ """ Test the support for imported exceptions. """
+
+ # Enable exception support.
+ exceptions = True
+
+ def test_Exceptions(self):
+ """ Test the throwing of an imported exception. """
+
+ from handler_module import StdException
+ from thrower_module import throwException
+
+ self.assertRaises(StdException, throwException)
diff --git a/test/imported_exceptions/thrower_module.sip b/test/imported_exceptions/thrower_module.sip
new file mode 100644
index 0000000..6982195
--- /dev/null
+++ b/test/imported_exceptions/thrower_module.sip
@@ -0,0 +1,19 @@
+// The SIP implementation of the thrower_module test module.
+
+
+%Module(name=thrower_module)
+
+%Import handler_module.sip
+
+
+%ModuleHeaderCode
+
+void throwException()
+{
+ throw std::exception();
+};
+
+%End
+
+
+void throwException();
diff --git a/test/int_convertors/int_convertors.sip b/test/int_convertors/int_convertors_module.sip
similarity index 98%
rename from test/int_convertors/int_convertors.sip
rename to test/int_convertors/int_convertors_module.sip
index d2f5496..1b36eb8 100644
--- a/test/int_convertors/int_convertors.sip
+++ b/test/int_convertors/int_convertors_module.sip
@@ -1,4 +1,7 @@
-%Module(name=int_convertors)
+// The SIP implementation of the int_convertors_module tets module.
+
+
+%Module(name=int_convertors_module)
%PostInitialisationCode
diff --git a/test/int_convertors/test_int_convertors.py b/test/int_convertors/test_int_convertors.py
index 176a978..fe14bd8 100644
--- a/test/int_convertors/test_int_convertors.py
+++ b/test/int_convertors/test_int_convertors.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 utils import SIPTestCase
@@ -15,7 +15,7 @@ class IntConvertorsTestCase(SIPTestCase):
super().setUpClass()
- from .int_convertors import IntConvertors
+ from int_convertors_module import IntConvertors
# Compute the various test values based on the native sizes.
cls.CHAR_LOWER = IntConvertors.char_lower()
diff --git a/test/enums/__init__.py b/test/movable/__init__.py
similarity index 100%
copy from test/enums/__init__.py
copy to test/movable/__init__.py
diff --git a/test/movable/movable_module.sip b/test/movable/movable_module.sip
new file mode 100644
index 0000000..14ea49f
--- /dev/null
+++ b/test/movable/movable_module.sip
@@ -0,0 +1,127 @@
+// The SIP implementation of the movable_module test module.
+
+
+%Module(name=movable_module)
+
+
+template <TYPE>
+%MappedType std::unique_ptr<TYPE> /Movable, NoRelease, AllowNone/
+{
+%TypeHeaderCode
+#include <memory>
+#include <utility> // For std::move().
+%End
+
+%ConvertFromTypeCode
+ const sipTypeDef *td = sipFindType("TYPE*");
+
+ return sipConvertFromNewType(sipCpp->release(), td, NULL);
+%End
+
+%ConvertToTypeCode
+ const sipTypeDef *td = sipFindType("TYPE");
+
+ if (sipIsErr == NULL)
+ return sipCanConvertToType(sipPy, td, 0);
+
+ if (sipPy == Py_None)
+ return 1;
+
+ int state;
+ TYPE *t = reinterpret_cast<TYPE *>(sipConvertToType(sipPy, td, sipTransferObj, 0, &state, sipIsErr));
+
+ sipReleaseType(t, td, state);
+
+ if (*sipIsErr)
+ return 0;
+
+ *sipCppPtr = new std::unique_ptr<TYPE>(t);
+
+ return sipGetState(sipTransferObj);
+%End
+};
+
+
+%ModuleHeaderCode
+class AnObject
+{
+public:
+ AnObject(int i) : mI(i) {}
+ virtual ~AnObject() {}
+
+ int getValue() const
+ {
+ return mI;
+ }
+
+ void increment()
+ {
+ mI++;
+ }
+
+private:
+ int mI;
+};
+%End
+
+
+class AnObject
+{
+public:
+ AnObject(int i);
+ virtual ~AnObject();
+
+ int getValue() const;
+};
+
+
+%ModuleHeaderCode
+#include <memory>
+
+
+class ObjectWrapper
+{
+public:
+ ObjectWrapper() {};
+ ObjectWrapper(const ObjectWrapper& other) : mObject(new AnObject(*other.mObject)) {}
+ ObjectWrapper& operator=(const ObjectWrapper& other)
+ {
+ mObject.reset(new AnObject(*other.mObject));
+ return *this;
+ };
+ virtual ~ObjectWrapper() {}
+
+ int getObjectValue() const
+ {
+ return mObject ? mObject->getValue() : -1000;
+ }
+
+ std::unique_ptr<AnObject> takeObject()
+ {
+ return std::move(mObject);
+ }
+
+ void setObject(std::unique_ptr<AnObject> anObject)
+ {
+ mObject = std::move(anObject);
+ mObject->increment();
+ }
+
+private:
+ std::unique_ptr<AnObject> mObject;
+};
+%End
+
+
+class ObjectWrapper
+{
+public:
+ ObjectWrapper();
+ virtual ~ObjectWrapper();
+
+ int getObjectValue() const;
+
+ std::unique_ptr<AnObject> takeObject();
+
+ void setObject(std::unique_ptr<AnObject> anObject);
+};
diff --git a/test/movable/test_movable.py b/test/movable/test_movable.py
new file mode 100644
index 0000000..04aadb8
--- /dev/null
+++ b/test/movable/test_movable.py
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPTestCase
+
+
+class MovableTestCase(SIPTestCase):
+ """ Test the support for the /Movable/ mapped type annotation. (See
+ issue/60.)
+ """
+
+ def test_Movable(self):
+ """ Test the support for /Movable/. It also verifies the test
+ implementation of the std::unique_ptr mapped type.
+ """
+
+ from sys import getrefcount
+ from movable_module import AnObject, ObjectWrapper
+
+ ao = AnObject(3)
+ ow = ObjectWrapper()
+
+ # Test the value of the object.
+ self.assertEqual(ao.getValue(), 3)
+ self.assertEqual(getrefcount(ao), 2)
+
+ # Test an empty wrapper.
+ self.assertEqual(ow.getObjectValue(), -1000)
+
+ # Test an non-empty wrapper.
+ ow.setObject(ao)
+ self.assertEqual(getrefcount(ao), 3)
+ 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.assertRaises(RuntimeError, ao.getValue)
diff --git a/test/enums/__init__.py b/test/py_enums/__init__.py
similarity index 100%
rename from test/enums/__init__.py
rename to test/py_enums/__init__.py
diff --git a/test/enums/enums.sip b/test/py_enums/py_enums_module.sip
similarity index 96%
rename from test/enums/enums.sip
rename to test/py_enums/py_enums_module.sip
index 348d18a..99ae9d3 100644
--- a/test/enums/enums.sip
+++ b/test/py_enums/py_enums_module.sip
@@ -1,6 +1,7 @@
-// The bindings for testing support for enums.
+// The SIP implementation of the py_enums_module test module.
-%Module(name=enums)
+
+%Module(name=py_enums_module)
%ModuleHeaderCode
diff --git a/test/enums/test_enums.py b/test/py_enums/test_py_enums.py
similarity index 85%
rename from test/enums/test_enums.py
rename to test/py_enums/test_py_enums.py
index 95d92d2..49d9d94 100644
--- a/test/enums/test_enums.py
+++ b/test/py_enums/test_py_enums.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 enum import Enum, Flag, IntEnum, IntFlag
@@ -8,8 +8,11 @@ from enum import Enum, Flag, IntEnum, IntFlag
from utils import SIPTestCase
-class EnumsTestCase(SIPTestCase):
- """ Test the support for enums in ABI v13 and later. """
+class PyEnumsTestCase(SIPTestCase):
+ """ Test the support for Python enums in ABI v13 and later. """
+
+ # The ABI version to use.
+ abi_version = '13'
@classmethod
def setUpClass(cls):
@@ -17,7 +20,7 @@ class EnumsTestCase(SIPTestCase):
super().setUpClass()
- from .enums import EnumClass
+ from py_enums_module import EnumClass
class NamedEnumFixture(EnumClass):
""" A fixture for testing named enum values. """
@@ -64,7 +67,7 @@ class EnumsTestCase(SIPTestCase):
def test_ModuleAnon(self):
""" Test a module level anonymous enum. """
- from .enums import AnonMember
+ from py_enums_module import AnonMember
self.assertIsInstance(AnonMember, int)
self.assertEqual(AnonMember, 10)
@@ -72,7 +75,7 @@ class EnumsTestCase(SIPTestCase):
def test_ClassAnon(self):
""" Test a class level anonymous enum. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.assertIsInstance(EnumClass.ClassAnonMember, int)
self.assertEqual(EnumClass.ClassAnonMember, 40)
@@ -84,7 +87,7 @@ class EnumsTestCase(SIPTestCase):
def test_Enum_BaseType(self):
""" Test /BaseType=Enum/. """
- from .enums import EnumBase
+ from py_enums_module import EnumBase
self.assertTrue(issubclass(EnumBase, Enum))
self.assertFalse(issubclass(EnumBase, Flag))
@@ -94,7 +97,7 @@ class EnumsTestCase(SIPTestCase):
def test_Flag_BaseType(self):
""" Test /BaseType=Flag/. """
- from .enums import FlagBase
+ from py_enums_module import FlagBase
self.assertTrue(issubclass(FlagBase, Flag))
self.assertFalse(issubclass(FlagBase, IntEnum))
@@ -103,7 +106,7 @@ class EnumsTestCase(SIPTestCase):
def test_IntEnum_BaseType(self):
""" Test /BaseType=IntEnum/. """
- from .enums import IntEnumBase
+ from py_enums_module import IntEnumBase
self.assertFalse(issubclass(IntEnumBase, Flag))
self.assertTrue(issubclass(IntEnumBase, IntEnum))
@@ -112,7 +115,7 @@ class EnumsTestCase(SIPTestCase):
def test_IntFlag_BaseType(self):
""" Test /BaseType=IntFlag/. """
- from .enums import IntFlagBase
+ from py_enums_module import IntFlagBase
self.assertFalse(issubclass(IntFlagBase, IntEnum))
self.assertTrue(issubclass(IntFlagBase, IntFlag))
@@ -124,7 +127,7 @@ class EnumsTestCase(SIPTestCase):
def test_ModuleNamed(self):
""" Test a module level named enum. """
- from .enums import NamedEnum
+ from py_enums_module import NamedEnum
self.assertTrue(issubclass(NamedEnum, Enum))
self.assertEqual(NamedEnum.NamedMember.value, 20)
@@ -132,7 +135,7 @@ class EnumsTestCase(SIPTestCase):
def test_ClassNamed(self):
""" Test a class level named enum. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.assertTrue(issubclass(EnumClass.ClassNamedEnum, Enum))
self.assertEqual(EnumClass.ClassNamedEnum.ClassNamedMember.value, 50)
@@ -140,7 +143,7 @@ class EnumsTestCase(SIPTestCase):
def test_named_get_member(self):
""" named enum virtual result with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.install_hook()
self.assertEqual(self.member_fixture.named_get(),
@@ -150,7 +153,7 @@ class EnumsTestCase(SIPTestCase):
def test_named_set_member(self):
""" named enum function argument with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.member_fixture.named_set(
EnumClass.ClassNamedEnum.ClassNamedMember)
@@ -158,14 +161,14 @@ class EnumsTestCase(SIPTestCase):
def test_named_var_member(self):
""" named enum instance variable with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.member_fixture.named_var = EnumClass.ClassNamedEnum.ClassNamedMember
def test_named_overload_set(self):
""" overloaded named enum function argument. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.member_fixture.named_overload_set(
EnumClass.ClassNamedEnum.ClassNamedMember)
@@ -174,7 +177,7 @@ class EnumsTestCase(SIPTestCase):
def test_named_get_int(self):
""" named enum virtual result with an integer value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
with self.assertRaises(TypeError):
self.install_hook()
@@ -200,7 +203,7 @@ class EnumsTestCase(SIPTestCase):
def test_ModuleScoped(self):
""" Test a module level C++11 scoped enum. """
- from .enums import ScopedEnum
+ from py_enums_module import ScopedEnum
self.assertTrue(issubclass(ScopedEnum, Enum))
self.assertEqual(ScopedEnum.ScopedMember.value, 30)
@@ -208,7 +211,7 @@ class EnumsTestCase(SIPTestCase):
def test_ClassScoped(self):
""" Test a class level C++11 scoped enum. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.assertTrue(issubclass(EnumClass.ClassScopedEnum, Enum))
self.assertEqual(EnumClass.ClassScopedEnum.ClassScopedMember.value, 70)
@@ -216,7 +219,7 @@ class EnumsTestCase(SIPTestCase):
def test_scoped_get_member(self):
""" scoped enum virtual result with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.install_hook()
self.assertIs(self.scoped_enum_fixture.scoped_get(),
@@ -226,7 +229,7 @@ class EnumsTestCase(SIPTestCase):
def test_scoped_set_member(self):
""" scoped enum function argument with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.scoped_enum_fixture.scoped_set(
EnumClass.ClassScopedEnum.ClassScopedMember)
@@ -234,6 +237,6 @@ class EnumsTestCase(SIPTestCase):
def test_scoped_var_member(self):
""" scoped enum instance variable with a member value. """
- from .enums import EnumClass
+ from py_enums_module import EnumClass
self.scoped_enum_fixture.scoped_var = EnumClass.ClassScopedEnum.ClassScopedMember
diff --git a/test/template_superclasses/__init__.py b/test/template_superclasses/__init__.py
new file mode 100644
index 0000000..6f186ca
--- /dev/null
+++ b/test/template_superclasses/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
diff --git a/test/template_superclasses/template_superclasses_module.sip b/test/template_superclasses/template_superclasses_module.sip
new file mode 100644
index 0000000..e139e76
--- /dev/null
+++ b/test/template_superclasses/template_superclasses_module.sip
@@ -0,0 +1,71 @@
+// The SIP implementation of the template_superclasses_module test module.
+
+
+%Module(name=template_superclasses_module)
+
+
+%ModuleHeaderCode
+
+class BaseClass
+{
+};
+
+template <class T1>
+class ValueWrapper : public BaseClass
+{
+public:
+ T1 getValue() const {
+ return T1();
+ }
+};
+
+template <class T2>
+class ExtendedValueWrapper : public ValueWrapper<T2>
+{
+};
+
+class AValue
+{
+};
+
+class BValue
+{
+};
+
+typedef ExtendedValueWrapper<AValue> AValueWrapper;
+typedef ExtendedValueWrapper<BValue> BValueWrapper;
+
+%End
+
+
+class BaseClass
+{
+};
+
+
+template <T1>
+class ValueWrapper : public BaseClass
+{
+public:
+ T1 getValue() const;
+};
+
+
+template <T2>
+class ExtendedValueWrapper : public ValueWrapper<T2>
+{
+};
+
+
+class AValue
+{
+};
+
+
+class BValue
+{
+};
+
+
+typedef ExtendedValueWrapper<AValue> AValueWrapper;
+typedef ExtendedValueWrapper<BValue> BValueWrapper;
diff --git a/test/template_superclasses/test_template_superclasses.py b/test/template_superclasses/test_template_superclasses.py
new file mode 100644
index 0000000..32fb877
--- /dev/null
+++ b/test/template_superclasses/test_template_superclasses.py
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPTestCase
+
+
+class TemplateSuperclassesTestCase(SIPTestCase):
+ """ Test the support for template arguments as super-classes in a template
+ class. (See issue/12.)
+ """
+
+ def test_TemplateSuperclasses(self):
+ """ Test the use of template arguments as super-classes. """
+
+ from template_superclasses_module import (AValue, AValueWrapper,
+ BaseClass, BValue, BValueWrapper)
+
+ self.assertTrue(issubclass(AValueWrapper, BaseClass))
+ a = AValueWrapper()
+ self.assertIsInstance(a.getValue(), AValue)
+
+ self.assertTrue(issubclass(BValueWrapper, BaseClass))
+ b = BValueWrapper()
+ self.assertIsInstance(b.getValue(), BValue)
diff --git a/test/timelines/__init__.py b/test/timelines/__init__.py
new file mode 100644
index 0000000..6f186ca
--- /dev/null
+++ b/test/timelines/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
diff --git a/test/timelines/test_timelines.py b/test/timelines/test_timelines.py
new file mode 100644
index 0000000..e0f26fd
--- /dev/null
+++ b/test/timelines/test_timelines.py
@@ -0,0 +1,28 @@
+# SPDX-License-Identifier: BSD-2-Clause
+
+# Copyright (c) 2025 Phil Thompson <phil at riverbankcomputing.com>
+
+
+from utils import SIPTestCase
+
+
+class TimelinesTestCase(SIPTestCase):
+ """ Test the support for timelines. """
+
+ # The list of tags.
+ tags = ['v2']
+
+ def test_Timelines(self):
+ """ Test the support for timelines. """
+
+ import timelines_module
+
+ mod_dict = timelines_module.__dict__
+
+ self.assertIn('between_v1_and_v3_enabled', mod_dict)
+ self.assertIn('up_to_unknown_enabled', mod_dict)
+
+ self.assertNotIn('up_to_v2_disabled', mod_dict)
+ self.assertNotIn('v3_and_after_disabled', mod_dict)
+ self.assertNotIn('unknown_and_after_disabled', mod_dict)
+ self.assertNotIn('between_unknown1_and_unknown2_disabled', mod_dict)
diff --git a/test/timelines/timelines_module.sip b/test/timelines/timelines_module.sip
new file mode 100644
index 0000000..f3ff6ca
--- /dev/null
+++ b/test/timelines/timelines_module.sip
@@ -0,0 +1,50 @@
+// The SIP implementation of the timelines_module test module.
+
+
+%Module(name=timelines_module)
+
+
+%Timeline {v1 v2 v3}
+
+
+%ModuleHeaderCode
+
+// We implement them all.
+void up_to_v2_disabled() {}
+void v3_and_after_disabled() {}
+void between_v1_and_v3_enabled() {}
+void up_to_unknown_enabled() {}
+void unknown_and_after_disabled() {}
+void between_unknown1_and_unknown2_disabled() {}
+
+%End
+
+
+%If (- v2)
+void up_to_v2_disabled();
+%End
+
+
+%If (v3 -)
+void v3_and_after_disabled();
+%End
+
+
+%If (v1 - v3)
+void between_v1_and_v3_enabled();
+%End
+
+
+%If (- unknown)
+void up_to_unknown_enabled();
+%End
+
+
+%If (unknown -)
+void unknown_and_after_disabled();
+%End
+
+
+%If (unknown1 - unknown2)
+void between_unknown1_and_unknown2_disabled();
+%End
diff --git a/test/utils/sip_test_case.py b/test/utils/sip_test_case.py
index b1ff2eb..c3310e9 100644
--- a/test/utils/sip_test_case.py
+++ b/test/utils/sip_test_case.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>
import glob
@@ -9,105 +9,62 @@ import os
import shutil
import subprocess
import sys
+import tarfile
import unittest
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. Note
- # that the enum tests are specific to ABI v13.
- #abi_version = None
- abi_version = '13.8'
- #abi_version = '12.15'
+ # The ABI version to use. None implies the latest major version.
+ abi_version = None
- @classmethod
- def setUpClass(cls):
- """ Build a test extension module. """
-
- # Get the name of the test directory from the file implementing the
- # test case.
- test_dir = os.path.dirname(inspect.getfile(cls))
-
- # Look for a .sip file in the test directory.
- sip_files = glob.glob(os.path.join(test_dir, '*.sip'))
-
- if len(sip_files) != 1:
- raise Exception(
- "expected to find a single .sip file in '{0}'".format(
- test_dir))
+ # Set if exception support should be enabled.
+ exceptions = False
- module_name = os.path.basename(sip_files[0])[:-4]
+ # The list of tags to be used to configure the test modules.
+ tags = None
- # Create a pyproject.toml file.
- pyproject_toml = os.path.join(test_dir, 'pyproject.toml')
+ # Set if a separate sip module should be generated. It will be built
+ # automatically if more than one module is being built.
+ use_sip_module = False
- with open(pyproject_toml, 'w') as f:
- f.write(_PYPROJECT_TOML.format(module_name=module_name))
+ @classmethod
+ def setUpClass(cls):
+ """ Build a test extension module. """
- if cls.abi_version is not None:
- f.write(_ABI_VERSION.format(abi_version=cls.abi_version))
+ # Get the name of the test's root directory from the file implementing
+ # the test case.
+ root_dir = os.path.dirname(inspect.getfile(cls))
- # Build the extension module.
- cwd = os.getcwd()
- subprocess.run([sys.executable, '-m', 'sipbuild.tools.build', '--verbose'], cwd=test_dir).check_returncode()
- os.chdir(cwd)
+ cls._modules = []
- # Move the extension module to the test directory.
- build_dir = os.path.join(test_dir, 'build')
-
- # The distutils and setuptools builders leave the module in different
- # places.
- for subdirs in ((module_name, ), (module_name, 'build', 'lib*')):
- module_pattern = [build_dir]
- module_pattern.extend(subdirs)
- module_pattern.append(
- module_name + '*.pyd' if sys.platform == 'win32' else '*.so')
-
- module_path = glob.glob(os.path.join(*module_pattern))
- if len(module_path) != 0:
- break
- else:
- raise Exception(
- "no '{0}' extension module was built".format(module_name))
+ sip_files = glob.glob(os.path.join(root_dir, '*.sip'))
+ use_sip_module = cls.use_sip_module or len(sip_files) > 1
- if len(module_path) != 1:
- raise Exception(
- "unable to determine file name of the '{0}' extension module".format(module_name))
+ # Build a sip module if required.
+ if use_sip_module:
+ cls._modules.append(cls._build_sip_module(root_dir))
- module_path = module_path[0]
- module_impl = os.path.join(test_dir, os.path.basename(module_path))
+ for sip_file in sip_files:
+ cls._modules.append(
+ cls._build_test_module(sip_file, use_sip_module, root_dir))
- # On Windows the module may be lying around from a previous run.
- try:
- os.remove(module_impl)
- except:
- pass
-
- os.rename(module_path, module_impl)
-
- # Remove the build artifacts.
- os.remove(pyproject_toml)
- shutil.rmtree(build_dir, ignore_errors=True)
-
- # Provide tearDownClass() with the values it needs to tidy up.
- cls._module_name = module_name
- cls._module_impl = module_impl
+ # Fix the path.
+ sys.path.insert(0, root_dir)
@classmethod
def tearDownClass(cls):
- """ Unload the module and remove the extension module. """
+ """ Unload the module. """
- try:
- del sys.modules[cls._module_name]
- except KeyError:
- pass
+ for module_name in cls._modules:
+ try:
+ del sys.modules[module_name]
+ except KeyError:
+ pass
- # This will fail on Windows (probably because the module is loaded).
- try:
- os.remove(cls._module_impl)
- except:
- pass
+ # Restore the path.
+ del sys.path[0]
def install_hook(self):
""" Install a hook that will catch any exceptions raised by a Python
@@ -137,6 +94,135 @@ 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. """
+
+ sip_module_name = 'sip'
+
+ # Create the sdist.
+ args = [sys.executable, '-m', 'sipbuild.tools.module', '--sdist',
+ '--target-dir', root_dir
+ ]
+
+ if cls.abi_version is not None:
+ args.append('--abi-version')
+ args.append(cls.abi_version)
+
+ args.append(sip_module_name)
+
+ subprocess.run(args).check_returncode()
+
+ # Find the sdist and unpack it.
+ sdists = glob.glob(
+ os.path.join(root_dir, sip_module_name + '-*.tar.gz'))
+
+ if len(sdists) != 1:
+ raise Exception(
+ "unable to determine the name of the sip module sdist file")
+
+ sdist = sdists[0]
+
+ with tarfile.open(sdist) as tf:
+ tf.extractall(path=root_dir, filter='data')
+
+ # 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,
+ 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 = """
@@ -149,8 +235,13 @@ 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