Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
762128e
#20 Set up coverage measurement and Codecov reporting
kwabenantim Jul 28, 2026
c62437c
#20 Cover the Custom generator hook defaults
kwabenantim Jul 28, 2026
1598723
#20 Cover small info and writer modules
kwabenantim Jul 28, 2026
9b99ccc
#20 Cover the CLI entry point (parse_args, generate, main)
kwabenantim Jul 28, 2026
e96c31c
#20 Cover utility functions in utils.py
kwabenantim Jul 28, 2026
c17cea3
#20 Fix KeyError when a config lists explicit variables
kwabenantim Jul 28, 2026
f074e09
#20 Cover the config parser
kwabenantim Jul 28, 2026
8b8b44b
#20 Cover module_info discovery and update paths
kwabenantim Jul 28, 2026
c695b17
#20 Cover class_info signature, namespace and name-building paths
kwabenantim Jul 28, 2026
27abde1
#20 Cover exception and typecaster resolution in package_info
kwabenantim Jul 28, 2026
c4ed949
#20 Cover the free-function and constructor writers
kwabenantim Jul 28, 2026
26e2d53
#20 Cover the method writer
kwabenantim Jul 28, 2026
45c49e0
#20 Cover class_writer virtual overrides
kwabenantim Jul 28, 2026
6352eea
#20 Measure coverage from the shapes and cells example runs
kwabenantim Jul 28, 2026
0eae7e2
#20 Cover generator construction and source-parser error paths
kwabenantim Jul 28, 2026
ac2ffe8
#20 Cover class_writer write() orchestration
kwabenantim Jul 28, 2026
65d43a7
#20 Cover writer and info edge paths
kwabenantim Jul 28, 2026
486f32e
#20 Fix dead generated-file skip in source collection
kwabenantim Jul 29, 2026
4ca85a7
#20 Share template-signature bracket stripping
kwabenantim Jul 29, 2026
4fc4ac9
#20 Cover package_info discovery and source collection
kwabenantim Jul 29, 2026
ceb676a
#20 Cover parser, writer and module-info edge branches
kwabenantim Jul 29, 2026
45ae0a5
#20 Fix broken default PackageInfo when no config file is given
kwabenantim Jul 29, 2026
69b6639
#20 Cover generator logging and class_writer base/include edges
kwabenantim Jul 29, 2026
e8c9517
#20 Cover remaining reachable writer and info edge paths
kwabenantim Jul 29, 2026
3afb435
#20 Reflect the achieved coverage level in codecov config
kwabenantim Jul 29, 2026
261978b
#20 Set the project coverage target to 99%
kwabenantim Jul 29, 2026
5d5f527
#20 Strip whitespace exposed by removing angle brackets
kwabenantim Jul 29, 2026
f77ad1e
#101 Parse exclude_inherited_overrides at module and class level
kwabenantim Jul 29, 2026
ebd13a6
#20 Cover abstract-base and inherited-override paths via the shapes e…
kwabenantim Jul 29, 2026
a769aa5
#20 Cover abstract-base and inherited-override paths via the cells ex…
kwabenantim Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion .github/workflows/test-cells-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Install cppwg
run: |
python3 -m pip install --upgrade pip
python3 -m pip install .
python3 -m pip install -e . coverage

- name: Install requirements
run: |
Expand Down Expand Up @@ -76,7 +76,17 @@ jobs:
cmake ..
working-directory: examples/cells

- name: Enable subprocess coverage
if: matrix.python-version == '3.12'
run: |
site=$(python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
echo "import coverage; coverage.process_startup()" > "$site/cov_subprocess.pth"
printf '[run]\nparallel = true\nsource = cppwg\n' > "$GITHUB_WORKSPACE/.cov_sub.rc"

- name: Regenerate wrappers
env:
COVERAGE_PROCESS_START: ${{ github.workspace }}/.cov_sub.rc
COVERAGE_FILE: ${{ github.workspace }}/.coverage
run: |
rm -rf dynamic/wrappers
cd build
Expand All @@ -89,6 +99,23 @@ jobs:
grep "Unknown class" build/cppwg.log
working-directory: examples/cells

- name: Generate coverage report
if: matrix.python-version == '3.12'
env:
COVERAGE_FILE: ${{ github.workspace }}/.coverage
run: |
python3 -m coverage combine --rcfile=.cov_sub.rc
python3 -m coverage xml --rcfile=.cov_sub.rc

- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
flags: cells
fail_ci_if_error: false

- name: Build
run: |
rattler-build build --recipe recipe/recipe.yaml -m variants/python${{ matrix.python-version }}.yaml --output-dir ./build --channel conda-forge
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/test-shapes-pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ jobs:
- name: Install cppwg
run: |
python3 -m pip install --upgrade pip
python3 -m pip install .
python3 -m pip install -e . coverage

- name: Regenerate wrappers
run: |
rm -rf wrapper/*/
cppwg src/cpp \
python3 -m coverage run --source=cppwg -m cppwg src/cpp \
--wrapper_root wrapper/ \
--package_info wrapper/package_info.yaml \
--includes src/cpp/*/ \
Expand All @@ -58,6 +58,20 @@ jobs:
grep "Unknown class" cppwg.log
working-directory: examples/shapes

- name: Generate coverage report
if: matrix.python-version == '3.12'
run: python3 -m coverage xml
working-directory: examples/shapes

- name: Upload coverage to Codecov
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./examples/shapes/coverage.xml
flags: shapes
fail_ci_if_error: false

- name: Configure
run: |
mkdir build && cd build
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ jobs:

- name: Run unit tests
run: python3 -m pytest tests/

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
flags: unit
fail_ci_if_error: false
32 changes: 32 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Codecov configuration.
#
# Coverage (issue #20) sits at ~99%. The project target is fixed at 99% and
# every patch must be fully covered, so new code keeps the total there. The
# small remainder is unreachable defensive code and one-way/loop-back branch
# halves that are not worth contriving inputs for; they are left uncovered
# rather than hidden behind coverage pragmas.
coverage:
status:
project:
default:
target: 99%
threshold: 0.5%
patch:
default:
target: 100%

# Coverage is uploaded from several jobs, each with its own flag: unit (the
# pytest suite), shapes and cells (real generator runs on the example projects).
# Codecov merges them per commit. carryforward keeps a flag's last coverage when
# its job does not run on a given commit, so the combined total stays stable.
flag_management:
default_rules:
carryforward: true
individual_flags:
- name: unit
- name: shapes
- name: cells

comment:
layout: "reach, diff, flags, files"
require_changes: true
8 changes: 6 additions & 2 deletions cppwg/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,12 @@ def parse_package_info(self) -> None:
self.package_info = info_parser.parse()

else:
# If no package info file exists, create a PackageInfo object with default settings
self.package_info = PackageInfo("cppwg_package", self.source_root)
# If no package info file exists, create a PackageInfo object with
# default settings. PackageInfo's second argument is a config dict,
# so pass the source root as a config entry rather than as the dict.
self.package_info = PackageInfo(
"cppwg_package", {"source_root": self.source_root}
)

def write_header_collection(self) -> None:
"""
Expand Down
1 change: 1 addition & 0 deletions cppwg/info/base_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ def __init__(self, name: str, info_config: dict[str, Any] | None = None) -> None
"custom_generator",
"discover_arg_excludes",
"discover_template_instantiations",
"exclude_inherited_overrides",
"excluded",
"excluded_methods",
"excluded_variables",
Expand Down
6 changes: 4 additions & 2 deletions cppwg/info/package_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,11 @@ def collect_source_files(
):
continue

# Skip files with the extensions like .cppwg.hpp
# Skip generated wrapper files (e.g. Foo.cppwg.hpp), whose
# inner extension is ".cppwg". os.path.splitext keeps the
# leading dot, so compare against ".cppwg" not "cppwg".
suffix = os.path.splitext(os.path.splitext(filename)[0])[1]
if suffix == CPPWG_EXT:
if suffix == f".{CPPWG_EXT}":
continue

filepaths.append(filepath)
Expand Down
4 changes: 3 additions & 1 deletion cppwg/parsers/package_info_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def parse(self) -> PackageInfo:
# Get module config from the raw module info
module_config = {
"name": "cppwg_module",
"exclude_inherited_overrides": None,
"external_bases": [],
"imports": [],
"source_locations": [],
Expand Down Expand Up @@ -173,6 +174,7 @@ def parse(self) -> PackageInfo:
for raw_class_info in module_config["classes"]:
# Get class config from the raw class info
class_config = {
"exclude_inherited_overrides": None,
"name_override": "",
"source_file": "",
"source_file_path": "",
Expand Down Expand Up @@ -260,7 +262,7 @@ def parse(self) -> PackageInfo:

# Create the CppVariableInfo object from the variable config dict
variable_info = CppVariableInfo(
variable_config["name"], variable_config
raw_variable_info["name"], variable_config
)

# Add the variable to the module
Expand Down
42 changes: 30 additions & 12 deletions cppwg/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,34 @@ def find_template_instantiations_in_source_file(
return True, find_template_instantiations_in_source(scan_source)


def strip_outer_angle_brackets(signature: str) -> str:
"""
Remove a single pair of enclosing angle brackets from a template signature.

e.g. "<unsigned A, unsigned B>" -> "unsigned A, unsigned B". A signature with
no surrounding brackets is returned unchanged (stripped of whitespace), so
callers can pass either form.

Parameters
----------
signature : str
A template signature, with or without its enclosing "<...>".

Returns
-------
str
The signature body without the outer angle brackets.
"""
inner = signature.strip()
if inner.startswith("<"):
inner = inner[1:]
if inner.endswith(">"):
inner = inner[:-1]
# Strip again: removing the brackets can expose whitespace that sat just
# inside them (e.g. "< a >" -> " a "), which the docstring promises to trim.
return inner.strip()


def parse_template_params(signature: str) -> list[str]:
"""
Extract template parameter names from a template signature.
Expand All @@ -488,13 +516,7 @@ def parse_template_params(signature: str) -> list[str]:
# Strip the outer angle brackets, then split on top-level commas only, so a
# comma inside a nested template (e.g. a default like "std::map<int, int>")
# does not split one parameter into two (see split_template_args).
inner = signature.strip()
if inner.startswith("<"):
inner = inner[1:]
if inner.endswith(">"):
inner = inner[:-1]

for part in split_template_args(inner):
for part in split_template_args(strip_outer_angle_brackets(signature)):
# e.g. "unsigned SPACE_DIM = 2" -> ["unsigned", "SPACE_DIM", "=", "2"].
# split() (no argument) splits on runs of arbitrary whitespace and drops
# empty tokens, so multiple spaces/tabs (e.g. "unsigned DIM") do not
Expand Down Expand Up @@ -612,11 +634,7 @@ def template_has_default_param(source: str, class_name: str) -> bool:
if signature is None:
return False

inner = signature
if inner.startswith("<"):
inner = inner[1:]
if inner.endswith(">"):
inner = inner[:-1]
inner = strip_outer_angle_brackets(signature)
return any("=" in part for part in split_template_args(inner))


Expand Down
13 changes: 13 additions & 0 deletions examples/cells/dynamic/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ typecasters:

modules:
- name: all
# SphericalMesh overrides virtuals that its abstract bases (AbstractMesh /
# AbstractSphericalMesh) already bind. With this set on the module, cppwg
# skips those redundant inherited-override bindings (e.g. PottsMesh::Scale
# and SphericalMesh::Scale, which AbstractMesh already binds).
exclude_inherited_overrides: True
source_locations:
- cell
- mesh
Expand All @@ -66,6 +71,14 @@ modules:
# mesh
- name: AbstractMesh

# AbstractSphericalMesh<E,S> is an abstract class with an abstract base
# (AbstractMesh); SphericalMesh<E,S> is a concrete leaf. Together they
# exercise abstract-with-abstract-base constructor skipping and, with the
# module's exclude_inherited_overrides, redundant-override skipping.
- name: AbstractSphericalMesh

- name: SphericalMesh

# Facet<DIM> references its faces Facet<DIM-1> only through pointers, and
# Facet<0> is never instantiated (see Facet.cpp). Wrapping Facet<2> is
# safe (its faces Facet<1> are instantiated). This substitution curates
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// This file is auto-generated by cppwg; manual changes will be overwritten.

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <memory>
#include "AbstractSphericalMesh.hpp"

#include "AbstractSphericalMesh.cppwg.hpp"

namespace py = pybind11;
PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr<T>);
typedef AbstractSphericalMesh<2, 2> AbstractSphericalMesh_2_2;
typedef AbstractSphericalMesh<3, 3> AbstractSphericalMesh_3_3;
typedef unsigned int unsignedint;


class AbstractSphericalMesh_2_2_Overrides : public AbstractSphericalMesh_2_2
{
public:
using AbstractSphericalMesh_2_2::AbstractSphericalMesh;
unsigned int GetNumElements() const override
{
PYBIND11_OVERRIDE_PURE(
unsignedint,
AbstractSphericalMesh_2_2,
GetNumElements,
);
}
};

void register_AbstractSphericalMesh_2_2_class(py::module &m)
{
py::class_<AbstractSphericalMesh_2_2, AbstractSphericalMesh_2_2_Overrides, std::shared_ptr<AbstractSphericalMesh_2_2>, AbstractMesh<2, 2>>(m, "AbstractSphericalMesh_2_2")
.def("GetNumElements",
(unsigned int(AbstractSphericalMesh_2_2::*)() const) &AbstractSphericalMesh_2_2::GetNumElements,
" ")
;
}


class AbstractSphericalMesh_3_3_Overrides : public AbstractSphericalMesh_3_3
{
public:
using AbstractSphericalMesh_3_3::AbstractSphericalMesh;
unsigned int GetNumElements() const override
{
PYBIND11_OVERRIDE_PURE(
unsignedint,
AbstractSphericalMesh_3_3,
GetNumElements,
);
}
};

void register_AbstractSphericalMesh_3_3_class(py::module &m)
{
py::class_<AbstractSphericalMesh_3_3, AbstractSphericalMesh_3_3_Overrides, std::shared_ptr<AbstractSphericalMesh_3_3>, AbstractMesh<3, 3>>(m, "AbstractSphericalMesh_3_3")
.def("GetNumElements",
(unsigned int(AbstractSphericalMesh_3_3::*)() const) &AbstractSphericalMesh_3_3::GetNumElements,
" ")
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This file is auto-generated by cppwg; manual changes will be overwritten.

#ifndef AbstractSphericalMesh_hpp__cppwg_wrapper
#define AbstractSphericalMesh_hpp__cppwg_wrapper

#include <pybind11/pybind11.h>

void register_AbstractSphericalMesh_2_2_class(pybind11::module &m);
void register_AbstractSphericalMesh_3_3_class(pybind11::module &m);
#endif // AbstractSphericalMesh_hpp__cppwg_wrapper
6 changes: 0 additions & 6 deletions examples/cells/dynamic/wrappers/all/PottsMesh.cppwg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ void register_PottsMesh_2_class(py::module &m)
{
py::class_<PottsMesh_2, PottsMesh_2_Overrides, std::shared_ptr<PottsMesh_2>, AbstractMesh<2, 2>>(m, "PottsMesh_2")
.def(py::init<>())
.def("Scale",
(void(PottsMesh_2::*)(double const)) &PottsMesh_2::Scale,
" ", py::arg("factor"))
;
}

Expand All @@ -56,8 +53,5 @@ void register_PottsMesh_3_class(py::module &m)
{
py::class_<PottsMesh_3, PottsMesh_3_Overrides, std::shared_ptr<PottsMesh_3>, AbstractMesh<3, 3>>(m, "PottsMesh_3")
.def(py::init<>())
.def("Scale",
(void(PottsMesh_3::*)(double const)) &PottsMesh_3::Scale,
" ", py::arg("factor"))
;
}
Loading
Loading