Skip to content

add cpp stub files - #4096

Merged
jhale merged 2 commits into
FEniCS:mainfrom
qbisi:cpp_stub
Feb 23, 2026
Merged

add cpp stub files#4096
jhale merged 2 commits into
FEniCS:mainfrom
qbisi:cpp_stub

Conversation

@qbisi

@qbisi qbisi commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Hi, this patch generate stub files for dolfinx.cpp module using nanobind's builtin nanobind_add_stub macro.
An extra marker file py.typed will be generated under site-packages/dolfinx/cpp/py.typed. Not sure if this is needed as we have a top-level one in site-packages/dolfinx/py.typed.

@schnellerhase schnellerhase added type-hints python Pull requests that update Python code labels Feb 18, 2026
@schnellerhase

Copy link
Copy Markdown
Contributor

Nice! Fixes #3238.

Not sure if this is needed as we have a top-level one in site-packages/dolfinx/py.typed.

No need, top-level will override anyways.

@jhale

jhale commented Feb 18, 2026

Copy link
Copy Markdown
Member

Thanks for doing this!

We should sanity check the output particularly for NDArrays in/out - wjakob/nanobind#1155

Comment thread python/CMakeLists.txt
@schnellerhase

schnellerhase commented Feb 18, 2026

Copy link
Copy Markdown
Contributor

Output looks good to me, for example mesh.pyi:

Details
"""Mesh library module"""

from collections.abc import Callable, Sequence
import enum
from typing import Annotated, overload

import numpy
from numpy.typing import NDArray

import dolfinx.cpp.common
import dolfinx.cpp.fem
import dolfinx.cpp.graph


class CellType(enum.Enum):
    point = 1

    interval = 2

    triangle = 3

    quadrilateral = -4

    tetrahedron = 4

    pyramid = -5

    prism = -6

    hexahedron = -8

    @property
    def name(self) -> object: ...

def to_type(cell: str) -> CellType: ...

def to_string(type: CellType) -> str: ...

def is_simplex(type: CellType) -> bool: ...

def cell_entity_type(type: CellType, dim: int, index: int) -> CellType: ...

def cell_dim(type: CellType) -> int: ...

def cell_num_entities(type: CellType, dim: int) -> int: ...

def cell_num_vertices(type: CellType) -> int: ...

def get_entity_vertices(type: CellType, dim: int) -> dolfinx.cpp.graph.AdjacencyList_int32: ...

def extract_topology(cell_type: CellType, layout: dolfinx.cpp.fem.ElementDofLayout, cells: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.int64]: ...

@overload
def build_dual_graph(comm: MPICommWrapper, cell_type: CellType, cells: dolfinx.cpp.graph.AdjacencyList_int64, max_facet_to_cell_links: int | None) -> dolfinx.cpp.graph.AdjacencyList_int64:
    """Build dual graph for cells"""

@overload
def build_dual_graph(comm: MPICommWrapper, cell_types: Sequence[CellType], cells: Sequence[Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]], max_facet_to_cell_links: int | None) -> dolfinx.cpp.graph.AdjacencyList_int64: ...

class GhostMode(enum.Enum):
    none = 0

    shared_facet = 1

def compute_entities(topology: Topology, dim: int, entity_type: CellType, num_threads: int = 1) -> tuple[list[dolfinx.cpp.graph.AdjacencyList_int32], dolfinx.cpp.graph.AdjacencyList_int32, dolfinx.cpp.common.IndexMap, list[int]]: ...

def compute_connectivity(topology: Topology, d0: Sequence[int], d1: Sequence[int]) -> list[dolfinx.cpp.graph.AdjacencyList_int32]: ...

class EntityMap:
    """EntityMap object"""

    def __init__(self, topology: Topology, sub_topology: Topology, dim: int, sub_topology_to_topology: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> None: ...

    def sub_topology_to_topology(self, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], inverse: bool) -> NDArray[numpy.int32]: ...

    @property
    def dim(self) -> int: ...

    @property
    def topology(self) -> Topology: ...

    @property
    def sub_topology(self) -> Topology: ...

class Topology:
    """Topology object"""

    def __init__(self, cell_type: CellType, vertex_map: dolfinx.cpp.common.IndexMap, cell_map: dolfinx.cpp.common.IndexMap, cells: dolfinx.cpp.graph.AdjacencyList_int32, original_index: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)] | None) -> None: ...

    def create_entities(self, dim: int, num_threads: int = 1) -> bool: ...

    def create_entity_permutations(self) -> None: ...

    def create_connectivity(self, d0: int, d1: int) -> None: ...

    def get_facet_permutations(self) -> Annotated[NDArray[numpy.uint8], dict(writable=False)]: ...

    def get_cell_permutation_info(self) -> Annotated[NDArray[numpy.uint32], dict(writable=False)]: ...

    @property
    def dim(self) -> int:
        """Topological dimension"""

    @property
    def original_cell_index(self) -> Annotated[NDArray[numpy.int64], dict(writable=False)]: ...

    @original_cell_index.setter
    def original_cell_index(self, original_cell_indices: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> None: ...

    @property
    def original_cell_indices(self) -> list[Annotated[NDArray[numpy.int64], dict(writable=False)]]: ...

    @overload
    def connectivity(self, d0: int, d1: int) -> dolfinx.cpp.graph.AdjacencyList_int32: ...

    @overload
    def connectivity(self, d0: Sequence[int], d1: Sequence[int]) -> dolfinx.cpp.graph.AdjacencyList_int32: ...

    def index_map(self, dim: int) -> dolfinx.cpp.common.IndexMap: ...

    def index_maps(self, dim: int) -> list[dolfinx.cpp.common.IndexMap]: ...

    @property
    def cell_type(self) -> CellType: ...

    @property
    def cell_types(self) -> list[CellType]: ...

    @property
    def entity_types(self) -> list[list[CellType]]: ...

    def interprocess_facets(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    @property
    def comm(self) -> MPICommWrapper: ...

def create_topology(arg0: MPICommWrapper, arg1: Sequence[CellType], arg2: Sequence[Sequence[int]], arg3: Sequence[Sequence[int]], arg4: Sequence[Sequence[int]], arg5: Sequence[int], /) -> Topology:
    """Create a Topology object."""

def compute_mixed_cell_pairs(arg0: Topology, arg1: CellType, /) -> list[list[int]]: ...

class MeshTags_int8:
    """MeshTags object"""

    def __init__(self, arg0: Topology, arg1: int, arg2: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.int8], dict(shape=(None,), order='C', writable=False)], /) -> None: ...

    @property
    def dtype(self) -> str: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

    @property
    def dim(self) -> int: ...

    @property
    def topology(self) -> Topology: ...

    @property
    def values(self) -> Annotated[NDArray[numpy.int8], dict(writable=False)]: ...

    @property
    def indices(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def find(self, arg: int, /) -> NDArray[numpy.int32]: ...

@overload
def create_meshtags(arg0: Topology, arg1: int, arg2: dolfinx.cpp.graph.AdjacencyList_int32, arg3: Annotated[NDArray[numpy.int8], dict(shape=(None,), order='C', writable=False)], /) -> MeshTags_int8: ...

@overload
def create_meshtags(arg0: Topology, arg1: int, arg2: dolfinx.cpp.graph.AdjacencyList_int32, arg3: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], /) -> MeshTags_int32: ...

@overload
def create_meshtags(arg0: Topology, arg1: int, arg2: dolfinx.cpp.graph.AdjacencyList_int32, arg3: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], /) -> MeshTags_int64: ...

@overload
def create_meshtags(arg0: Topology, arg1: int, arg2: dolfinx.cpp.graph.AdjacencyList_int32, arg3: Annotated[NDArray[numpy.float64], dict(shape=(None,), order='C', writable=False)], /) -> MeshTags_float64: ...

class MeshTags_int32:
    """MeshTags object"""

    def __init__(self, arg0: Topology, arg1: int, arg2: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], /) -> None: ...

    @property
    def dtype(self) -> str: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

    @property
    def dim(self) -> int: ...

    @property
    def topology(self) -> Topology: ...

    @property
    def values(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    @property
    def indices(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def find(self, arg: int, /) -> NDArray[numpy.int32]: ...

class MeshTags_int64:
    """MeshTags object"""

    def __init__(self, arg0: Topology, arg1: int, arg2: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], /) -> None: ...

    @property
    def dtype(self) -> str: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

    @property
    def dim(self) -> int: ...

    @property
    def topology(self) -> Topology: ...

    @property
    def values(self) -> Annotated[NDArray[numpy.int64], dict(writable=False)]: ...

    @property
    def indices(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def find(self, arg: int, /) -> NDArray[numpy.int32]: ...

class MeshTags_float64:
    """MeshTags object"""

    def __init__(self, arg0: Topology, arg1: int, arg2: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.float64], dict(shape=(None,), order='C', writable=False)], /) -> None: ...

    @property
    def dtype(self) -> str: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

    @property
    def dim(self) -> int: ...

    @property
    def topology(self) -> Topology: ...

    @property
    def values(self) -> Annotated[NDArray[numpy.float64], dict(writable=False)]: ...

    @property
    def indices(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def find(self, arg: float, /) -> NDArray[numpy.int32]: ...

class Geometry_float32:
    """Geometry object"""

    def __init__(self, index_map: dolfinx.cpp.common.IndexMap, dofmap: Annotated[NDArray[numpy.int32], dict(shape=(None, None), order='C', writable=False)], element: dolfinx.cpp.fem.CoordinateElement_float32, x: Annotated[NDArray[numpy.float32], dict(shape=(None, None), writable=False)], input_global_indices: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> None: ...

    @property
    def dim(self) -> int:
        """Geometric dimension"""

    @property
    def dofmap(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def dofmaps(self, i: int) -> Annotated[NDArray[numpy.int32], dict(writable=False)]:
        """
        Get the geometry dofmap associated with coordinate element i (mixed topology)
        """

    def index_map(self) -> dolfinx.cpp.common.IndexMap: ...

    @property
    def x(self) -> Annotated[NDArray[numpy.float32], dict(shape=(None, 3))]:
        """
        Return coordinates of all geometry points. Each row is the coordinate of a point.
        """

    @property
    def cmap(self) -> dolfinx.cpp.fem.CoordinateElement_float32:
        """The coordinate map"""

    def cmaps(self, arg: int, /) -> dolfinx.cpp.fem.CoordinateElement_float32:
        """The ith coordinate map"""

    @property
    def input_global_indices(self) -> Annotated[NDArray[numpy.int64], dict(writable=False)]: ...

class Mesh_float32:
    """Mesh object"""

    def __init__(self, comm: MPICommWrapper, topology: Topology, geometry: Geometry_float32) -> None: ...

    @property
    def geometry(self) -> Geometry_float32:
        """Mesh geometry"""

    @property
    def topology(self) -> Topology:
        """Mesh topology"""

    @property
    def comm(self) -> MPICommWrapper: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

def create_interval_float32(comm: MPICommWrapper, n: int, p: Sequence[float], ghost_mode: GhostMode, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None) -> Mesh_float32: ...

def create_rectangle_float32(comm: MPICommWrapper, p: Sequence[Sequence[float]], n: Sequence[int], celltype: CellType, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None, diagonal: DiagonalType) -> Mesh_float32: ...

def create_box_float32(comm: MPICommWrapper, p: Sequence[Sequence[float]], n: Sequence[int], celltype: CellType, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None) -> Mesh_float32: ...

@overload
def create_mesh(arg0: MPICommWrapper, arg1: Sequence[Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]], arg2: Sequence[dolfinx.cpp.fem.CoordinateElement_float32], arg3: Annotated[NDArray[numpy.float32], dict(order='C', writable=False)], arg4: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32], arg5: int | None) -> Mesh_float32: ...

@overload
def create_mesh(comm: MPICommWrapper, cells: Annotated[NDArray[numpy.int64], dict(shape=(None, None), order='C', writable=False)], element: dolfinx.cpp.fem.CoordinateElement_float32, x: Annotated[NDArray[numpy.float32], dict(order='C', writable=False)], partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None, max_facet_to_cell_links: int | None) -> Mesh_float32:
    """Helper function for creating meshes."""

@overload
def create_mesh(arg0: MPICommWrapper, arg1: Sequence[Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]], arg2: Sequence[dolfinx.cpp.fem.CoordinateElement_float64], arg3: Annotated[NDArray[numpy.float64], dict(order='C', writable=False)], arg4: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32], arg5: int | None) -> Mesh_float64: ...

@overload
def create_mesh(comm: MPICommWrapper, cells: Annotated[NDArray[numpy.int64], dict(shape=(None, None), order='C', writable=False)], element: dolfinx.cpp.fem.CoordinateElement_float64, x: Annotated[NDArray[numpy.float64], dict(order='C', writable=False)], partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None, max_facet_to_cell_links: int | None) -> Mesh_float64:
    """Helper function for creating meshes."""

@overload
def create_submesh(mesh: Mesh_float32, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> tuple[Mesh_float32, EntityMap, EntityMap, NDArray[numpy.int32]]: ...

@overload
def create_submesh(mesh: Mesh_float64, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> tuple[Mesh_float64, EntityMap, EntityMap, NDArray[numpy.int32]]: ...

@overload
def cell_normals(mesh: Mesh_float32, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float32]: ...

@overload
def cell_normals(mesh: Mesh_float64, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float64]: ...

@overload
def h(mesh: Mesh_float32, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float32]:
    """Compute maximum distsance between any two vertices."""

@overload
def h(mesh: Mesh_float64, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float64]: ...

@overload
def compute_midpoints(mesh: Mesh_float32, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float32]: ...

@overload
def compute_midpoints(mesh: Mesh_float64, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)]) -> NDArray[numpy.float64]: ...

@overload
def locate_entities(mesh: Mesh_float32, dim: int, marker: Callable[[Annotated[NDArray[numpy.float32], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]]) -> NDArray[numpy.int32]: ...

@overload
def locate_entities(mesh: Mesh_float32, dim: int, marker: Callable[[Annotated[NDArray[numpy.float32], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]], entity_type_idx: int) -> NDArray[numpy.int32]: ...

@overload
def locate_entities(mesh: Mesh_float64, dim: int, marker: Callable[[Annotated[NDArray[numpy.float64], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]]) -> NDArray[numpy.int32]: ...

@overload
def locate_entities(mesh: Mesh_float64, dim: int, marker: Callable[[Annotated[NDArray[numpy.float64], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]], entity_type_idx: int) -> NDArray[numpy.int32]: ...

@overload
def locate_entities_boundary(mesh: Mesh_float32, dim: int, marker: Callable[[Annotated[NDArray[numpy.float32], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]]) -> NDArray[numpy.int32]: ...

@overload
def locate_entities_boundary(mesh: Mesh_float64, dim: int, marker: Callable[[Annotated[NDArray[numpy.float64], dict(shape=(None, None), writable=False)]], Annotated[NDArray[numpy.bool_], dict(shape=(None,), order='C')]]) -> NDArray[numpy.int32]: ...

@overload
def entities_to_geometry(mesh: Mesh_float32, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], permute: bool) -> NDArray[numpy.int32]: ...

@overload
def entities_to_geometry(mesh: Mesh_float64, dim: int, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], permute: bool) -> NDArray[numpy.int32]: ...

@overload
def create_geometry(arg0: Topology, arg1: Sequence[dolfinx.cpp.fem.CoordinateElement_float32], arg2: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], arg4: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', writable=False)], arg5: int, /) -> Geometry_float32: ...

@overload
def create_geometry(arg0: Topology, arg1: Sequence[dolfinx.cpp.fem.CoordinateElement_float64], arg2: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], arg3: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)], arg4: Annotated[NDArray[numpy.float64], dict(shape=(None,), order='C', writable=False)], arg5: int, /) -> Geometry_float64: ...

class Geometry_float64:
    """Geometry object"""

    def __init__(self, index_map: dolfinx.cpp.common.IndexMap, dofmap: Annotated[NDArray[numpy.int32], dict(shape=(None, None), order='C', writable=False)], element: dolfinx.cpp.fem.CoordinateElement_float64, x: Annotated[NDArray[numpy.float64], dict(shape=(None, None), writable=False)], input_global_indices: Annotated[NDArray[numpy.int64], dict(shape=(None,), order='C', writable=False)]) -> None: ...

    @property
    def dim(self) -> int:
        """Geometric dimension"""

    @property
    def dofmap(self) -> Annotated[NDArray[numpy.int32], dict(writable=False)]: ...

    def dofmaps(self, i: int) -> Annotated[NDArray[numpy.int32], dict(writable=False)]:
        """
        Get the geometry dofmap associated with coordinate element i (mixed topology)
        """

    def index_map(self) -> dolfinx.cpp.common.IndexMap: ...

    @property
    def x(self) -> Annotated[NDArray[numpy.float64], dict(shape=(None, 3))]:
        """
        Return coordinates of all geometry points. Each row is the coordinate of a point.
        """

    @property
    def cmap(self) -> dolfinx.cpp.fem.CoordinateElement_float64:
        """The coordinate map"""

    def cmaps(self, arg: int, /) -> dolfinx.cpp.fem.CoordinateElement_float64:
        """The ith coordinate map"""

    @property
    def input_global_indices(self) -> Annotated[NDArray[numpy.int64], dict(writable=False)]: ...

class Mesh_float64:
    """Mesh object"""

    def __init__(self, comm: MPICommWrapper, topology: Topology, geometry: Geometry_float64) -> None: ...

    @property
    def geometry(self) -> Geometry_float64:
        """Mesh geometry"""

    @property
    def topology(self) -> Topology:
        """Mesh topology"""

    @property
    def comm(self) -> MPICommWrapper: ...

    @property
    def name(self) -> str: ...

    @name.setter
    def name(self, arg: str, /) -> None: ...

def create_interval_float64(comm: MPICommWrapper, n: int, p: Sequence[float], ghost_mode: GhostMode, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None) -> Mesh_float64: ...

def create_rectangle_float64(comm: MPICommWrapper, p: Sequence[Sequence[float]], n: Sequence[int], celltype: CellType, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None, diagonal: DiagonalType) -> Mesh_float64: ...

def create_box_float64(comm: MPICommWrapper, p: Sequence[Sequence[float]], n: Sequence[int], celltype: CellType, partitioner: Callable[[MPICommWrapper, int, Sequence[CellType], Sequence[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32] | None) -> Mesh_float64: ...

@overload
def create_cell_partitioner(mode: GhostMode, max_facet_to_cell_links: int | None) -> Callable[[MPICommWrapper, int, list[CellType], list[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32]:
    """Create default cell partitioner."""

@overload
def create_cell_partitioner(part: Callable[[MPICommWrapper, int, dolfinx.cpp.graph.AdjacencyList_int64, bool], dolfinx.cpp.graph.AdjacencyList_int32], ghost_mode: GhostMode, max_facet_to_cell_links: int | None) -> Callable[[MPICommWrapper, int, list[CellType], list[Annotated[NDArray[numpy.int64], dict(writable=False)]]], dolfinx.cpp.graph.AdjacencyList_int32]:
    """Create a cell partitioner from a graph partitioning function."""

def exterior_facet_indices(topology: Topology) -> NDArray[numpy.int32]: ...

def compute_incident_entities(mesh: Topology, entities: Annotated[NDArray[numpy.int32], dict(shape=(None,), order='C', writable=False)], d0: int, d1: int) -> NDArray[numpy.int32]: ...

class DiagonalType(enum.Enum):
    left = 0

    right = 1

    crossed = 2

    left_right = 4

    right_left = 5

@schnellerhase

Copy link
Copy Markdown
Contributor

Thanks for doing this!

We should sanity check the output particularly for NDArrays in/out - wjakob/nanobind#1155

Seems to be not triggered for us. Maybe because of the no convert() usages.

@schnellerhase schnellerhase left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should go in after FEniCS/basix#991.

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
@jhale

jhale commented Feb 23, 2026

Copy link
Copy Markdown
Member

Now should go in after FEniCS/basix#970

@jhale

jhale commented Feb 23, 2026

Copy link
Copy Markdown
Member

Looks good - as an aside we need to guard the Spack build cache uploads for PRs coming from external repositories.

Merged via the queue into FEniCS:main with commit bd66574 Feb 23, 2026
17 of 19 checks passed
@francesco-ballarin

Copy link
Copy Markdown
Member

FYI, downstream checks in a nightly docker image are failing with

/usr/local/dolfinx-real/lib/python3.12/dist-packages/dolfinx/cpp/common.pyi:105: error:
Invalid syntax  [syntax]
        def global_to_local(self, global: Annotated[NDArray[numpy.int64], ...
                                   ^
Found 1 error in 1 file (errors prevented further checking)

I guess that the issue is that global is a reserved keyword.

@jhale

jhale commented Feb 24, 2026

Copy link
Copy Markdown
Member

Thanks - I think this is not being picked up because our mypy checks are before the compile-time generation of the typing stubs. Can you confirm @francesco-ballarin ?

@francesco-ballarin

Copy link
Copy Markdown
Member

This CI step

- name: Run mypy # Use a venv to avoid NumPy upgrades that are incompatible with numba
happens after the compile time generation, but is checking the source directory rather than the installation directory, which indeed results in the typing stubs not being checked.

jhale added a commit that referenced this pull request Feb 24, 2026
@jhale

jhale commented Feb 24, 2026

Copy link
Copy Markdown
Member

Going to revert this see #4100 - needs a bit more debugging and testing, e.g. editable installs.

github-merge-queue Bot pushed a commit that referenced this pull request Feb 24, 2026
schnellerhase pushed a commit that referenced this pull request Feb 24, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
@jhale

jhale commented Feb 25, 2026

Copy link
Copy Markdown
Member

This should be re-worked following the implementation in FEniCS/basix#992 which doesn't require e.g. INSTALL_TIME on Unix-like systems.

However, I do see an issue with using nanobind_add_stub for DOLFINx. There are some HPCs I've used where it is mandatory to place an MPI process inside e.g. srun and from mpi4py import MPI before doing import dolfinx.cpp or import cpp to stubgen. So for DOLFINx, we should make it possible to totally disable wrapper generation, and ideally, we should control how we launch the stubgen Python process, see e.g.:

https://github.com/jorgensd/dolfinx_mpc/blob/main/python/CMakeLists.txt#L72

@jhale

jhale commented Feb 25, 2026

Copy link
Copy Markdown
Member

We might be best off patching nanobind to allow more control:

https://github.com/wjakob/nanobind/blob/master/cmake/nanobind-config.cmake#L703

schnellerhase pushed a commit that referenced this pull request Mar 1, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
schnellerhase pushed a commit that referenced this pull request Mar 18, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
schnellerhase pushed a commit that referenced this pull request Mar 24, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
schnellerhase pushed a commit that referenced this pull request May 16, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
schnellerhase pushed a commit that referenced this pull request May 16, 2026
* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.
pull Bot pushed a commit to gnikit/dolfinx that referenced this pull request Aug 1, 2026
)

* add cpp stub files (FEniCS#4096)

* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.

* Fix: reserved python global keyword

* Fix: non-install time for UNIX platforms + CI adaptation

* Fix: scoped log import

* fix: petsc...

* Work in progress on autogenerating nanobind stubs

* Generate dolfinx.cpp stubs automatically

* Add a way to disable nanobind stubgen (e.g. HPC builds)

* Fix formatting

* Fix on platforms without petsc4py

* Use spack-fenics due to updated base deps

* Back to main

* Fix.

* Revert

* Fix Windows module name

* Fix mypy failures surfaced by nanobind stub generation

The auto-generated dolfinx.cpp stubs made mypy see real, precise types
for the compiled extension for the first time, surfacing ~446 errors
in the "Build and test" CI job (the only mypy invocation that actually
installs dolfinx before running mypy, so the only one exercising the
generated stubs). Root causes and fixes:

- Unix stub generation imported the compiled module as bare `cpp`
  instead of `dolfinx.cpp`, so nanobind wrote cross-submodule
  references like `import cpp.la`, which doesn't exist and silently
  resolved to Any under mypy, masking real errors and producing bogus
  "overload can never match" diagnostics. Stage the built module under
  a throwaway dolfinx/ package dir before invoking nanobind_add_stub
  so the module's real __name__ is dolfinx.cpp.
- `_IntegralType`/`MPICommWrapper` bindings used names or const_name()
  values with no resolvable Python type, breaking stub cross-refs.
- `FiniteElement`/`AdjacencyList` `__eq__` bindings exposed the raw C++
  operator==, violating object.__eq__'s Liskov contract; wrapped in a
  lambda with an isinstance guard instead.
- Dead, unreachable overloads (complex instantiations of
  interpolation_matrix/discrete_curl/discrete_gradient that are always
  shadowed by the real ones, and a redundant read_geometry_data
  registration) removed.
- Missing nanobind/stl/map.h and .../string.h includes were causing
  stubgen to fall back to invalid raw C++ type strings.
- ~250 Python-side errors are the same runtime-safe-but-statically-
  unverifiable scalar-type dispatch pattern already handled elsewhere
  in this PR (fem/petsc.py); fixed with matching `# type: ignore`.
  A handful were real bugs instead: wrong return-type annotations in
  forms.py, and dispatch-table locals missing an explicit Union
  annotation.
- The ~24 remaining errors are genuine ecosystem-level gaps confirmed
  via isolated repros (mypy can't disambiguate numpy dtype/rank in
  NDArray annotations; nanobind has no std::reference_wrapper caster;
  basix's C++ types aren't stub-resolvable across the package
  boundary) rather than dolfinx bugs. Suppressed per-file via a new
  nanobind stub pattern file (stub_patterns.txt) that injects a
  scoped `# mypy: disable-error-code=...` into just the affected
  generated stubs.

Verified: mypy clean (matching the failing job's exact PETSc/ADIOS2
config), ruff/clang-format clean, and a second full build with
PETSc+SLEPc+ADIOS2+ParMETIS+SuperLU_DIST (-Werror) compiles cleanly
with runtime sanity checks passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Extend mypy type-ignore fixes for new/changed main-branch APIs

Post-merge fixups: main added a mesh argument to pack_coefficients,
an interpolate_geometry function, and reshaped a few overload call
sites (dofmaps as a sequence instead of a method, apply_lifting's
now-arg-type instead of call-overload mismatch). Same runtime-safe/
statically-unverifiable scalar-dispatch pattern as the rest of this
branch; verified mypy clean again afterwards.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Format python/CMakeLists.txt with gersemi

Fixes Lint CI failure introduced by the main merge, which brought in
the gersemi CMake formatter (replacing cmake-format). Purely cosmetic
reformatting of the nanobind stub-generation additions; no logic
changes.

* Fix mypy errors only visible in package-mode type checking

The "Build and test" CI job runs mypy in package mode
(`mypy -p dolfinx`) against a genuinely installed dolfinx with real
nanobind-generated stubs, unlike the Lint job's `mypy dolfinx`, which
never builds/installs dolfinx and so resolves dolfinx.cpp.* as Any via
ignore_missing_imports, masking overload-resolution errors entirely.
Package mode surfaced 34 real errors invisible in Lint mode:

- bcs.py/assemble.py: the DirichletBC/insert_diagonal overload
  mismatches report as call-overload in package mode, not the
  arg-type code the ignores were scoped to; switch to codeless
  ignores since the reported code is unstable across the two modes.
- io/utils.py: new arg-type error on write_function's getattr cast.
- mesh.py: refine()'s partitioner annotation didn't include None,
  even though the docstring documents None as a valid value and
  callers (test_refinement.py) pass it.
- test_mesh_partitioners.py: overly narrow inferred list element type
  broke when a ParameterSet skip-marker was appended.
- test_mesh.py: partitioner_kahip/partitioner_parmetis are only
  present on the compiled module when built with those optional
  backends, which mypy can't know statically.
- demo_mixed-topology.py, demo_static-condensation.py: same
  dtype-union-overload ambiguity pattern fixed elsewhere in this PR.

Verified via a from-scratch venv replicating the CI job exactly
(Python 3.12, non-editable install, real generated stubs): mypy passes
in all three invocation modes (Lint's mypy dolfinx/test/demo, and
package-mode -p dolfinx/test/demo), ruff check/format clean, and the
affected test files pass under pytest.

* Also ignore partitioner_scotch attr-defined in test_mesh.py

The CI runner for the "Build and test" job doesn't have libscotch
installed, so partitioner_scotch is absent from the compiled module
there too (unlike my local reproduction, which has SCOTCH via
Homebrew) -- same build-config-dependent attribute-existence issue as
partitioner_kahip/partitioner_parmetis fixed in the previous commit.

* Remove two avoidable mypy ignores

- fem/utils.py: narrow space0/space1's cpp objects via a match
  statement in interpolation_matrix so mypy can resolve the matching
  interpolation_matrix overload directly, instead of ignoring the
  call. Also gives callers a clear TypeError on mismatched dtypes
  instead of an opaque nanobind overload-resolution failure.
- io/gmsh.py: read_from_msh's partitioner parameter typed its
  callback's 4th argument as AdjacencyList (the pure-Python wrapper,
  unused elsewhere in this file) instead of _AdjacencyList_int32 (the
  cpp type that model_to_mesh, which it delegates to, actually
  expects). Fixing the annotation removes the genuine type mismatch
  instead of suppressing it.

* Switch VTKFile/XDMFFile to composition instead of subclassing cpp types

VTKFile and XDMFFile subclassed their nanobind-bound counterparts
directly (_cpp.io.VTKFile/_cpp.io.XDMFFile), unlike Mesh, Function,
FunctionSpace, and VTXWriter, which all wrap their cpp object via a
_cpp_object attribute instead. Subclassing meant every overridden
method that re-types its arguments from the raw cpp type to the
friendlier Python wrapper type (Mesh vs Mesh_float32/64, etc.) was a
genuine Liskov substitution violation from mypy's point of view,
requiring a # type: ignore[override] on write_mesh, write_meshtags,
write_function, and read_meshtags.

Switching to composition removes the inheritance relationship, so
these methods are no longer checked against a base signature and the
four ignores are gone with no suppression left behind. This requires
explicit delegation for every previously-inherited method actually
used elsewhere (close, comm, flush, write_information,
read_information, read_topology_data, read_geometry_data,
read_cell_type), confirmed via a grep across tests and demos, plus
write_geometry for API parity even though nothing in-tree calls it.

read_meshtags's and the new write_geometry's underlying cpp functions
only support float64 meshes/geometry; narrowed via isinstance instead
of suppressing, giving a real static check and a clear TypeError for
the unsupported case instead of an ignore.

* Remove 3 more avoidable mypy ignores in io/vtkhdf.py

- read_mesh's filename was passed straight through as str | Path, but
  the cpp read_vtkhdf_mesh_float32/64 bindings only accept str; cast
  explicitly instead of ignoring, narrowing the float32 branch's
  ignore down to just the pre-existing, unrelated assignment error.
- variant (mesh_cpp.geometry.cmaps[0].variant) is a raw int from the
  cpp binding; basix.ufl.element expects a LagrangeVariant. Wrap it,
  matching the same conversion already used in fem/utils.py's
  interpolate_geometry.
- cell_types[0].name's ignore was stale: removing it produces no
  error, confirmed by rebuilding and rerunning mypy.

Found via a full audit: stripped every remaining arg-type ignore in
the tree at once, rebuilt against a from-scratch venv mirroring CI's
package-mode mypy check, and inspected every resulting error. Outside
of this file, everything else needs its ignore back - each is a
single or multiple _cpp_object-typed argument whose Python-level type
is a plain dtype union unconnected to any generic parameter (same
pattern as FunctionSpace/Mesh/Form discussed elsewhere), so narrowing
would mean adding an isinstance/match branch per dtype for no real
safety benefit. Left those as ignores rather than trade a suppression
for genuine complexity.

* Remove remaining ignore[assignment] in vtkhdf.read_mesh

mesh_cpp's type was inferred from its first assignment
(read_vtkhdf_mesh_float64 -> Mesh_float64), so the second branch's
Mesh_float32 assignment was flagged as incompatible. Declare the
union type explicitly up front, matching the same pattern already
used for the analogous float32/float64 dispatch in mesh.py's
create_interval/create_rectangle/create_box.

* Remove 5 stale arg-type ignores

Found via a systematic strip-and-rebuild audit of every remaining
ignore[arg-type] comment: these 5 produce no mypy error at all once
removed, in package mode, mypy test, and mypy demo. No code changes
beyond deleting the comments.

* Fix wrong partitioner Callable signature in gmsh.py

model_to_mesh's and read_from_msh's partitioner parameter was typed
as Callable[[Comm, int, int, AdjacencyList_int32], AdjacencyList_int32],
but that value is passed straight into create_mesh, whose cpp binding
requires Callable[[Comm, int, Sequence[CellType],
Sequence[NDArray[int64]]], AdjacencyList_int32] - matching what
dolfinx.mesh.create_cell_partitioner's return type already documents.
The 3rd/4th argument types were simply wrong, not an inherent
dtype-overload ambiguity.

* Remove 2 arg-type ignores in geometry.bb_tree

Branch on isinstance(mesh._cpp_object, Mesh_float32/64) instead of
np.issubdtype(mesh.geometry.x.dtype, ...) - same if/elif structure,
but discriminating on the actual value being passed to the cpp
constructor lets mypy narrow it through each branch.

* Remove arg-type ignore in mesh.create_geometry

The function already validates that element.dtype matches x.dtype
before construction, so element._cpp_object's own concrete type
already determines which Geometry class to build - dispatch off
isinstance(cpp_element, ...) directly instead of a separate ftype
lookup keyed on x.dtype. Removes a layer of indirection rather than
adding one.

Incidental fix: the "Unknown floating type for geometry" message was
missing its f-string prefix, so {x.dtype} was never interpolated.

* Remove 2 arg-type ignores in mesh.create_point_mesh

geometry was just constructed from points.dtype a few lines above, so
geometry._cpp_object's own concrete type already determines which
Mesh class to build; branch on isinstance(cpp_geometry, ...) instead
of points.dtype == ... for the same reason as the bb_tree/
create_geometry fixes.

* Remove arg-type ignore in mesh.create_cell_partitioner

@singledispatch requires the base function's part parameter to be
typed Callable | GhostMode (a supertype of the registered GhostMode
variant), but the base implementation is only ever reached when part
is genuinely a Callable - a GhostMode argument gets routed to the
registered variant instead. Add a defensive isinstance guard so mypy
narrows part to Callable for the rest of the function, matching what
was already true at runtime.

* Remove 2 arg-type ignores in graph.comm_graph_data/comm_to_json

Both are non-overloaded cpp functions accepting only
AdjacencyList_int_sizet_int8__int32_int32, and the only Python-level
producer of that type is comm_graph() (confirmed via the cpp stub:
its return type is unconditionally that one class). Add an isinstance
guard, giving callers a clear error instead of an opaque nanobind
overload failure if they pass an AdjacencyList from anywhere else.

* Remove arg-type ignore in LinearProblem's preconditioner form

form()'s parameter was typed ufl.Form | Sequence[ufl.Form] |
Sequence[Sequence[ufl.Form]] with no None, even though
LinearProblem's P (the preconditioner) is documented and typed as
optional, and _create_form's implementation already passes None
straight through unchanged via its final `else: return form` branch.
Widen the annotation to match what the implementation already does.

* Remove arg-type ignore in derivative_block's rank-one Jacobian branch

The rank-zero branch already guards u with isinstance(u, Function) /
isinstance(u, Sequence), raising a clear ValueError otherwise. The
rank-one branch called _derive_univariate_jacobian(F, u, du) (which
requires u: Function, du: ufl.Argument | None) with no equivalent
guard, so a caller passing u or du as a sequence would previously fall
through silently instead of getting the same clear error the sibling
branch gives.

* Remove arg-type ignore in LinearProblem.solve

mypy type-checks functools.singledispatch calls only against the base
function's signature (L: typing.Any, constants: npt.NDArray | None,
...), so passing self.L (a Form) as the second positional argument to
assemble_vector(self.b.array, self.L) was checked against
constants: npt.NDArray | None and flagged. self.b.array/self.L are
concretely npt.NDArray/Form here (not ambiguous unions), so calling
the registered variant _assemble_vector_array directly - the exact
function singledispatch would have dispatched to anyway - resolves
cleanly. Same pattern already used in fem/petsc.py for the same
functools.singledispatch/mypy limitation.

* Remove 6 arg-type ignores in fem/petsc.py assemble_vector/assemble_matrix

functools.singledispatch base functions recursively called their own
singledispatch wrapper to reach the registered PETSc.Vec/PETSc.Mat
variant, but mypy only checks singledispatch calls against the base
signature - which is shaped for the "no b/A supplied yet" case, not
the (b, L, ...)/(A, a, ...) shape actually being passed. Name the two
previously-anonymous registered variants (_assemble_vector_petsc,
_assemble_matrix_petsc) and call them directly at each of the 6 call
sites (the base function's own recursive call, the NEST sub-block
recursion, and 4 call sites in LinearProblem/assemble_residual/
NewtonSolverNonlinearProblem), bypassing the mismatched base check
entirely.

Also fixes a genuine pre-existing bug: the base assemble_matrix's
constants/coeffs parameter types didn't match its own
a: Form | Sequence[Sequence[Form]] shape (Sequence[X]/Sequence[dict]
instead of bare X/Sequence[Sequence[dict]]), inconsistent with the
correctly-typed registered sibling - this was the source of 2 of the
6 errors surviving the rename alone.

Adds one small isinstance(coeffs, dict) guard in the NEST-matrix
branch, mirroring the existing isinstance(a, Sequence) check right
above it, for a genuinely-reachable misuse case.

Verified against a from-scratch PETSc-enabled build of this worktree
(petsc4py isn't available in the mypy-checking venv used elsewhere in
this branch's history): assemble_vector/assemble_matrix in both their
new-object and existing-object forms, LinearProblem.solve(),
assemble_residual, NewtonSolverNonlinearProblem.J, NEST-matrix
assembly, and the new guard's error path.

* Remove 3 more arg-type ignores in fem/petsc.py assemble_vector

Same root cause as assemble_matrix's NEST branch: constants/coeffs
still carry their full declared union type (including shapes meant
for the other branches) at each _assemble_vector_array call site,
since narrowing L doesn't narrow the separate constants/coeffs
parameters. Add isinstance guards mirroring the existing
isinstance(L, Sequence) checks - a bare dict in the NEST/block
branches, or a Sequence in the single-form branch, is a genuine
caller error these now catch explicitly instead of silently
misbehaving.

Verified against a from-scratch PETSc-enabled build: NEST vector
assembly, block-offset vector assembly, single-form assembly, and
both new guards' error paths.

* Remove arg-type ignore in set_bc's NEST recursion

The outer isinstance(bcs[0], Sequence) check establishes that bcs is
genuinely 2D before reaching the NEST branch, but mypy can't propagate
a check on an element (bcs[0]) to narrow bcs's own declared type. Add
a per-iteration isinstance(bc, Sequence) guard instead, which mypy can
use to narrow bc directly for the recursive set_bc call.

Verified against a from-scratch PETSc-enabled build: NEST set_bc with
correctly-nested bcs, and the new guard's error path with malformed
input.

* Remove 5 arg-type ignores in LinearProblem/assemble_residual block paths

Two genuine pre-existing bugs surfaced once investigated:

- LinearProblem.a/.preconditioner properties were typed
  Form | Sequence[Form], but __init__ actually accepts and stores
  Form | Sequence[Sequence[Form]] (a: ufl.Form | Sequence[Sequence[
  ufl.Form]]), matching the class docstring's a_ij(u, v) block-matrix
  description. L is correctly 1D as declared.

- extract_function_spaces's third @typing.overload declared
  -> list[list[FunctionSpace | None]] for 2D forms, but the
  implementation's 2D branch returns list(unique_spaces(V)) - a flat
  list, same shape as the second overload. Confirmed against the
  existing test_extract_function_spaces test, which indexes the
  result with Vc[0]/Vc[1], not Vc[0][0].

With both fixed, 3 call sites still needed a small isinstance guard
(LinearProblem's block and single-form branches, assemble_residual's
block branch), since self.a/self.L/residual are properties/parameters
that can't be narrowed by checking a different variable
(self.u/jacobian) - same idiom as the earlier set_bc/assemble_matrix
fixes.

Verified against a from-scratch PETSc-enabled build: a well-posed
block LinearProblem.solve() (correct solution norms, exact Dirichlet
BC enforcement), assemble_residual's block path, and the new guards'
error paths.

* Remove 16 type-ignore comments in fem/function.py

10 were stale - removing them produces no mypy error at all, in any
of the three checked modes.

The other 6 (in functionspace()) were caused by a real bug: the
function reused its own element parameter (typed
AbstractFiniteElement | ElementMetaData | tuple[...]) to hold the
result of finiteelement(...), a completely different type (the
compiled FiniteElement wrapper). Reassigning a parameter to an
incompatible type confuses mypy's flow analysis for every subsequent
use, not just the reassignment itself. Renamed to dolfinx_element.

Two remaining ignores in the same function (689, 699) are left alone:
a try/except TypeError duck-typing fallback that genuinely can't be
narrowed without restructuring the ElementMetaData conversion.

Verified: mypy clean in all three modes, ruff clean,
test_function.py/test_custom_basix_element.py pass (74 tests), and a
direct runtime check of functionspace() for both float32 and float64.

* Remove 16 type-ignore comments in mesh.py/fem/element.py, fix h()'s hardcoded dtype

9 were stale - removing them produces no mypy error in any of the
three checked modes.

4 more (coordinate_element's singledispatch base-vs-registered
mismatch, same limitation as assemble_vector/assemble_matrix fixed
earlier in petsc.py): named the anonymous registered variant
_coordinate_element_from_basix and called it directly at all 4
call sites in mesh.py.

3 more, from two real bugs in refine()/uniform_refine():
- refine()'s return type omitted | None for parent_cell/parent_facet
  even though its own docstring says "(optional) parent cells,
  (optional) parent facets" and the underlying cpp function genuinely
  returns NDArray | None for both.
- both functions accessed msh._ufl_domain.ufl_coordinate_element()
  without checking _ufl_domain (itself typed ufl.Mesh | None) isn't
  None first. Added explicit guards with a clear ValueError instead of
  a potential silent AttributeError.

Also fixed (but did not remove the ignore for) Mesh.h()'s return type,
hardcoded to npt.NDArray[np.float64] even though the underlying
_cpp.mesh.h is genuinely overloaded per dtype and Mesh is
Generic[Real] - changed to npt.NDArray[Real]. The ignore stays since
_cpp_object's type still isn't tied to Real (same architectural gap as
Geometry.x), but the annotation is now honest about float32 meshes.

Verified: mypy clean in all three modes, ruff clean, 433 tests pass,
and direct runtime checks of h() for both dtypes, normal
refine/uniform_refine, both new guards' error paths, and both
create_mesh code paths using the renamed function.

* Remove 4 arg-type ignores in fem/utils.py via match-based narrowing

create_interpolation_data, discrete_curl, discrete_gradient, and
interpolate_geometry each take two or three independent FunctionSpace/
Mesh/Geometry/CoordinateElement/FiniteElement objects that must share
a dtype for the underlying cpp overload to resolve - same shape as
interpolation_matrix, fixed earlier with the same technique. Narrow
via match/isinstance instead of ignoring, so a genuine dtype mismatch
between the objects now raises a clear TypeError instead of an opaque
nanobind overload failure.

Verified: mypy clean in all three modes, ruff clean, 121 PETSc tests
pass (test_petsc_discrete_operators.py, against a from-scratch
PETSc-enabled build) plus 215 (test_interpolation.py) + 28
(test_interpolate_geometry.py) non-PETSc tests.

* Remove 4 stale type-ignore comments in fem/bcs.py

These 4 (the non-Iterable-V early-return branches and the block-form
_V list comprehensions in locate_dofs_geometrical/locate_dofs_
topological) produce no mypy error at all once removed, in any of the
three checked modes.

The remaining 10 ignores in this file are genuine: DirichletBC.g/
function_space return the raw cpp object instead of the declared
wrapped Function/Constant/FunctionSpace type (g even has its own
"TODO: needs to be wrapped" comment - a known, deliberately-deferred
gap, not something to silently implement here), and dirichletbc()'s
_value/bctype correlation is intentional polymorphism across raw
arrays, Function, Constant, and scalar values, not a variable-reuse
bug.

Verified: mypy clean in all three modes, ruff clean, test_bcs.py
passes (24 tests), and a direct runtime check of both freed
block-form call paths.

* Remove more type: ignore comments in fem/forms.py

- extract_function_spaces: remove stale union-attr ignore (forms is
  already narrowed at this point).
- compile_form: replace assignment ignore with an explicit
  typing.cast, since ffcx.get_options() returns a heterogeneous dict.
- derivative_block: extend the isinstance-based du/u narrowing already
  used in the rank-one branch to the rank-zero and block-Jacobian
  branches, removing the three remaining bare ignores.

* Fix singledispatch call-arg bug in create_cell_partitioner call sites

The @create_cell_partitioner.register(GhostMode) variant was anonymous
(named _), so mypy checked its call sites against the 3-argument base
function's signature instead of the actual 2-argument dispatched
overload, producing a bogus "Missing positional argument" error at
every call site. Name the registered function and call it directly at
the two internal call sites (mesh.create_mesh, XDMFFile.read_mesh),
removing both ignores. Also drop an inert '# F401' comment left over
on the VTXMeshPolicy import (ruff confirms the import is used).

* Fix same create_cell_partitioner call-arg bug in demo_mixed-topology.py

Same root cause as the previous mesh.py/io/utils.py fix: call the named
GhostMode-dispatch variant directly instead of through the generic
singledispatch function, whose base signature mypy incorrectly checks
call sites against.

* Fix same create_cell_partitioner call-arg bug in demo_axis.py/demo_pml.py

Same root cause and fix as the previous two commits: call the named
GhostMode-dispatch variant directly instead of through the generic
singledispatch function.

* Remove stale type: ignore in plot.py

Once the @overload/@singledispatch attr-defined error on vtk_mesh.register
fires, mypy no longer independently checks the registered function body,
making its own ignore comment redundant.

* Replace var-annotated ignores with explicit type hints in demo_tnt-elements.py

Empty-list literals can't have their element type inferred by mypy;
annotate x/M as list[list[np.ndarray]] instead of suppressing.

* Guard against None function space in NewtonSolver.__init__

extract_function_spaces(problem.L) is statically Optional; add an
explicit None check before calling create_vector, which requires a
non-optional FunctionSpace for its single-space overload.

* Correct type: ignore error codes in VTXWriter for ADIOS2-enabled builds

Verified against a real ADIOS2+petsc4py build: the previous ignore
codes (attr-defined only, union-attr) were only correct for the
no-ADIOS2 build and silently did nothing once ADIOS2 attributes
genuinely exist. Add the assignment/arg-type codes that actually fire
in an ADIOS2-enabled build, and add a missing ignore on the
Function-sequence VTXWriter constructor call.

* Remove stale type: ignore comments in la/petsc.py

Verified against a real petsc4py build (by patching a locally-generated
stub defect that was blocking mypy analysis entirely -- not part of
this diff): PETSc is always a real, unconditionally-imported module in
this file (guarded only by a runtime RuntimeError, never
TYPE_CHECKING), so none of the name-defined/attr-defined ignores were
ever needed. Only createGhostWithArray/createGhost's argument
type mismatches and one singledispatch dispatch-type mismatch are
genuine; corrected their codes and line placement to match where mypy
actually reports them.

* Correct type: ignore comments in nls/petsc.py

Verified against a real petsc4py build: the A/b property ignores were
stale (PETSc.Mat/Vec are real types here). solve/setP genuinely violate
the Liskov substitution principle against the cpp base class's Vec/Mat
signatures by design (the Python wrapper takes Function/high-level
callables); give them the specific override code instead of a bare
ignore.

* Correct type: ignore comments in fem/petsc.py, fix singledispatch bugs

Verified against a real petsc4py+ADIOS2 build (by patching a locally
generated nanobind stub defect that was blocking mypy analysis
entirely -- not part of this diff) and the no-petsc4py build:

- 74 of 150 ignores were stale: PETSc.Vec/Mat/etc. are always real
  types here (petsc4py is unconditionally imported, never
  TYPE_CHECKING-gated), so the name-defined/attr-defined ignores from
  when this wasn't reliably checkable no longer apply.
- 10 ignores had the wrong error code and were silently doing nothing.
- 20 lines were missing ignores for errors that leaked through
  undetected.
- Fixed 3 real call sites (LinearProblem.solve, assemble_jacobian,
  NewtonSolverNonlinearProblem.F) that called the generic
  assemble_matrix/assemble_vector singledispatch functions positionally
  plus a bcs= keyword, which mypy correctly flags as a keyword
  conflict against the singledispatch base signature (Python's
  singledispatch itself dispatches fine at runtime, but mypy only
  checks calls against the un-registered base signature). Call
  _assemble_matrix_petsc/_assemble_vector_petsc directly instead,
  matching the pattern already used elsewhere in this file.

Verified with the full PETSc-marked pytest suite (174 passed) plus
direct runtime smoke tests of assemble_matrix, assemble_vector,
apply_lifting, LinearProblem.solve, and discrete_gradient.

* Fix type: ignore comments in demo_stokes.py

Verified against a real petsc4py build: numpy.dtype (PETSc.ScalarType's
declared stub type) is a valid DTypeLike, so np.zeros(..., dtype=...)
needed no ignore. Calling PETSc.ScalarType(0) as a constructor does
genuinely error against that same stub type; give it the operator code.

* Remove stale type: ignore comments in demo_matrix-free-petsc.py

Verified against a real petsc4py build: these zip() unpackings type
check cleanly once petsc4py's real stubs are available.

* Fix type: ignore comments in demo_static-condensation.py

Verified against a real petsc4py build: 12 of 15 ignores were stale.
bc.set(b) was missing an ignore -- DirichletBC.set expects an ndarray,
not the PETSc.Vec passed here.

* Fix type: ignore comments for jv() calls in EM scattering demos

Verified against a real petsc4py build: jv(nu, alpha) with a real alpha
type-checks fine; only jv(nu, m * alpha), where m is complex, needed an
ignore, with the call-overload code.

* Fix type: ignore comments in several demos

Verified against a real petsc4py build: the ScalarType import, PETSc.Sys()
and PETSc.Error except-clause, and float32-check ignores were all stale.
demo_pyamg.py's dirichletbc(value=dtype(0.0), ...) call genuinely errors
against a runtime-constructed dtype; give it the specific operator/misc
codes instead of a bare ignore.

* Fix type: ignore comments in demo_axis.py

Verified against a real petsc4py build: the complexfloating check was
stale. sys = PETSc.Sys()/hasExternalPackage genuinely error against the
petsc4py.PETSc module-vs-Sys-class stub; give them the specific
assignment/attr-defined codes.

* Remove stale type: ignore comments in demo_gmsh.py/demo_interpolation-io.py

ignore_missing_imports = true is a global [tool.mypy] setting shared by
every CI job's pyproject.toml, so an unstubbed import (gmsh) or an
attribute access on an object derived from one (pyvista's Plotter)
can never actually error under this config.

* Fix singledispatch call-arg bug in fem/problems.py LinearProblem.solve

Same root cause as the fem/petsc.py/mesh.py fixes: call the named
MatrixCSR-dispatch variant (_assemble_matrix_csr) directly instead of
through the generic singledispatch function, whose base signature mypy
incorrectly checks call sites against.

* Fix nanobind stub type names for PETSc Mat/Vec/IS/KSP casters

PETSC_CASTER_MACRO used bare identifiers (mat, vec, is, ksp) as the
nanobind stub type name, instead of fully-qualified petsc4py.PETSc.*
names like caster_mpi.h correctly does for mpi4py.MPI.Comm. This
produced invalid generated stubs everywhere these types appear
(la.petsc, fem.petsc, nls.petsc) -- including a literal Python syntax
error, since `is` is a keyword, that crashes mypy outright when
checking against a real PETSc-enabled build's stubs. This is almost
certainly why so much PETSc-touching code accumulated broad
`# type: ignore` comments: mypy against these types was never reliably
checkable to begin with.

Verified by rebuilding the nanobind extension and regenerating stubs
directly with nanobind.stubgen: la.petsc/fem.petsc/nls.petsc now emit
valid, correctly-qualified types with the petsc4py.PETSc import
auto-added. Full PETSc-marked pytest suite passes (174 tests).

* Fix two more nanobind stub type leaks in la.cpp and io.h

SparsityPattern's "concatenate sub-patterns" constructor took maps as a
raw std::reference_wrapper<const IndexMap> directly in the nanobind-
facing signature; nanobind has no caster that unwraps reference_wrapper
to its underlying (already-bound) type, so the stub fell back to a raw,
invalid C++ type-name string. Fixed following the pattern already used
for DirichletBC elsewhere (assemble.h): accept shared_ptr<const
IndexMap> at the binding boundary (which nanobind resolves natively)
and build the reference_wrapper internally before forwarding to the
real constructor. This was the sole cause of the
dolfinx.cpp.la.__prefix__ valid-type suppression in stub_patterns.txt,
confirmed by regenerating the stub and running mypy with the
suppression removed -- now deleted.

VTXWriter's Function-list constructor accepts all four scalar/geometry
combinations at the C++ level (matching the real, intentional
ADIOS2Writers.h API), but only the two matched-precision combinations
per geometry type are ever bound to a Python fem.Function class, so
the other two are Python-unreachable yet still leaked into the stub as
unresolvable raw type names. Give the two per-T overloads an nb::sig
override restricting the declared type to what's actually reachable;
the C++ overload itself is unchanged.

Verified by rebuilding, regenerating stubs directly via
nanobind.stubgen, confirming mypy -p dolfinx is clean, and running the
complete python/test suite (3108 passed, 92 skipped, 27 xfailed,
matching the pre-change baseline).

* Simplify nanobind stub generation in python/CMakeLists.txt

Hoist install(TARGETS cpp ...) out of the ENABLE_NANOBIND_STUBGEN
branches so the compiled module is always installed, even with stub
generation disabled. Factor the duplicated 17-entry .pyi OUTPUT list
into a single NANOBIND_STUB_OUTPUTS variable shared by the WIN32 and
UNIX nanobind_add_stub() calls.

* Always run nanobind stub generation, remove ENABLE_NANOBIND_STUBGEN option

Stub generation is not opt-out in practice (no CI job or packaging
path disables it), so the option only added an untested configuration
path. Run it unconditionally instead.

* Link nanobind_add_stub docs from the UNIX stub-staging comment

Points readers at the mechanism (stubgen imports MODULE and infers
output location from __file__) that motivates staging cpp under a
throwaway dolfinx/ package directory.

* Drop the symlink add_custom_command for UNIX stub staging

Instead of building cpp normally and symlinking it into a throwaway
dolfinx/ directory post-build, set the cpp target's
LIBRARY_OUTPUT_DIRECTORY to build directly into that directory.
install(TARGETS cpp ...) still locates the target correctly regardless
of its output directory, so nothing else needs to change.

Verified with a from-scratch build: cpp links directly into
dolfinx/cpp.<ext>, stub generation produces the same
dolfinx.cpp.<submodule>-style cross-references as before, and
`cmake --install` places the .so under dolfinx/ as expected.

* Use a regular (non-editable) install in the RHEL/Spack CI job

The "AlmaLinux build and test" job has been failing deterministically
on every run since the nanobind stub-generation work landed: every
demo fails at import time with "ImportError: cannot import name 'cpp'
from partially initialized module 'dolfinx' (most likely due to a
circular import)".

This job is the only CI job that installs dolfinx with `pip install
-e` (editable). All non-editable installs across the rest of CI (the
PETSc-enabled matrix in ccpp.yml, plus repeated local reproduction
with an editable-install of this exact branch) succeed reliably.

The new stub generation puts `dolfinx/cpp/*.pyi` (a directory of type
stubs, matching nanobind's own convention for a compiled extension
with nested submodules) directly alongside the compiled
`dolfinx/cpp.<ext>` module. scikit-build-core's editable-install
redirect builds a manifest that classifies each installed path as
either a "wheel file" (compiled/source module) or a namespace-package
search location; a `.pyi`-only directory that exactly shadows a
compiled module's own name is an edge case scikit-build-core's own
source comments show has caused prior classification bugs in this
exact area (upstream issues FEniCS#1427, FEniCS#1482). This is a good fit for what
we observe: the module resolves fine via the ordinary installed-path
loader, but not through the editable redirect on this platform.

This CI job doesn't need editable mode -- it builds once and
immediately runs demos/tests against that one build, with no
edit-and-rerun step in between -- so switching to a regular install
sidesteps the redirect entirely rather than chasing the exact
upstream classification bug.

* Fix editable installs by requiring scikit-build-core>=1.0.0

Root-cause fix, replacing the earlier non-editable CI workaround
(previous commit): editable installs were never actually broken by
this PR's own code, but by a real bug in scikit-build-core <1.0.0's
editable redirect finder.

Empirically bisected locally (macOS, reproduced 100% on 0.11.0 through
0.12.2, 0/10 failures from 1.0.0 onward): the pre-1.0 redirect finder
resolves a compiled module straight from its known file path via
importlib.util.spec_from_file_location, without checking what else is
on disk. nanobind's generated dolfinx/cpp/*.pyi stub directory (its
standard convention for a compiled extension with nested submodules)
sits right next to the compiled dolfinx/cpp.<ext> module, and the
pre-1.0 build-time manifest scan registers dolfinx.cpp both as a
"wheel file" (the .so) and, from the stub directory's __init__.pyi, as
a package with its own search location -- confusing every subsequent
`from dolfinx import cpp` in dolfinx/common.py. 1.0.0 resolves compiled
modules through PathFinder instead, which correctly prefers the real
file over the same-named stub directory regardless of the manifest
ambiguity.

Since this is a real upstream fix rather than a workaround, restore the
RHEL/Spack CI job's editable install. That job's pinned Spack package
repo only provides py-scikit-build-core up to 0.12.2 (confirmed by
checking out the exact packages_ref tag), so pip-upgrade scikit-build-core
to >=1.0.0 from PyPI specifically for that build step rather than
relying on the Spack-provided one.

* Fix two mypy ignores caused by name reuse across incompatible types

demo_axis.py reused the module-level `sys` (the stdlib module, used for
sys.argv) as a local PETSc.Sys() instance; mypy forbids narrowing a
name to an incompatible type within the same scope. Renamed to
petsc_sys, which needs no suppression at all.

fem/assemble.py's _assemble_matrix_csr had the same pattern on the
`bcs` parameter, reassigning it from Sequence[DirichletBC] | None to a
list of raw _cpp_object handles. Renamed to _bcs (matching the
existing convention in fem/petsc.py), which resolves the [misc]
redefinition error. The [arg-type] ignore on the following
_cpp.fem.assemble_matrix call stays -- confirmed via mypy that it
suppresses three separate, genuine dtype-Union-vs-concrete-overload
mismatches unrelated to the renaming.

Verified with ruff check/format and a targeted mypy run against the
built stubs: demo_axis.py now has zero errors, assemble.py's remaining
ignore is the minimal one needed.

* Fix genuinely-fixable mypy ignores in PETSc/scipy demos

- demo_pyamg.py: narrow poisson_problem's dtype parameter from
  npt.DTypeLike to type[np.floating] | type[np.complexfloating],
  matching how it's actually called. This also exposed that
  dirichletbc's own value type hint was too narrow -- it already
  handles anything with a .dtype attribute at runtime, just didn't
  declare it -- so widen fem/bcs.py's dirichletbc signature to include
  raw numpy scalars instead of reaching for a lossy .item() conversion
  (which would have silently upcast float32 boundary values to
  float64).
- demo_pml.py / demo_scattering-boundary-conditions.py: scipy-stubs
  does support complex arguments to jv, just typed as numpy.complex128
  /complex64, not builtin complex -- wrap m * alpha accordingly. This
  uncovered a real bug: compute_a was annotated -> float but always
  returns a genuinely complex Mie coefficient (callers already take
  np.real/np.abs of it) -- fixed to -> complex in both files.
- demo_mixed-topology.py: cast hexahedron/prism's _cpp_object to
  CoordinateElement_float64 (both are built with the default
  dtype=np.float64, so this matches runtime reality) instead of
  ignoring the dtype-Union mismatch. This gives create_mesh's return
  type real precision, which surfaced two more pre-existing errors
  further down the same file that were previously masked by the
  broken overload match; added targeted ignores for those (same
  wrapper-Union-vs-concrete-overload pattern as elsewhere, no clean
  local fix available).
- assemble.py: insert_diagonal was still passed the stale `bcs` name
  after the earlier _bcs rename, a runtime bug (TypeError) hidden by
  a bare `# type: ignore`; fixed to reference _bcs, with the ignore
  narrowed to [call-overload] to match the actual error code.

Verified with ruff check/format, mypy against the built stubs, and by
actually running demo_pyamg.py (all four dtypes, correct precision
preserved) and demo_mixed-topology.py (runs through everything touched
here; its pre-existing failure further on, unrelated to this change,
reproduces identically on unmodified main).

* Clarify DirichletBC::set docs on ghost/owned-only x and x0 length

Neither the Python nor the C++ doc comment previously explained why
passing x with or without ghost entries changes what set() does. Traced
the mechanism in DirichletBC.h's apply() lambda: _dofs0 always contains
both owned and ghost dof indices, and the per-entry bounds check
`_dofs0[i] < x.size()` is what makes an owned-only x safe (ghost
indices are simply skipped) as well as a full local+ghost x (both get
set). Also document that x0, when provided, must be at least as long
as x -- only checked via assert in Debug/Developer builds, not a
per-element bounds check like x itself.

* Fix Group C: use PETSc.Vec.array_w instead of widening DirichletBC.set

demo_static-condensation.py was the only demo passing a raw PETSc.Vec
directly to DirichletBC.set(), which only works at runtime because
nanobind's ndarray caster happens to accept anything satisfying the
buffer protocol -- but statically needs an npt.NDArray, and PETSc.Vec
isn't typed as satisfying that anywhere. Every other demo doing the
identical assemble_vector/apply_lifting/ghostUpdate/set sequence
(demo_elasticity.py, demo_stokes.py) already uses b.array_w for
exactly this call; demo_static-condensation.py had just missed it.

Widening fem/bcs.py's DirichletBC.set signature to accept a buffer-like
type was considered and rejected: the underlying nanobind binding's own
generated stub types x as a concrete ndarray[float64, ...] regardless,
so a Python-level widening would only relocate the mismatch rather than
resolve it, and would require either a hard petsc4py dependency (which
fem/bcs.py deliberately avoids) or a buffer-protocol Protocol requiring
Python's 3.12+ collections.abc.Buffer (project floor is 3.11).

Verified with ruff check/format and mypy against the built stubs; b
remains the same PETSc.Vec object afterward (array_w is a zero-copy
view), so the later solver.solve(b, ...) call is unaffected.

* Fix two real bugs found while reviewing bcs.py's type: ignore comments

locate_dofs_geometrical/locate_dofs_topological's docstrings claimed
that passing an iterable of function spaces returns "a 2-D array of
shape (number of dofs, 2)". This is wrong: both the C++
implementation (std::array<std::vector<int32_t>, 2>) and every actual
call site (test_bcs.py's dofs[0]/dofs[1] indexing) treat it as a list
of one array per space. Fixed the docstrings, and split each function
into @overload declarations so the return type (np.ndarray vs.
list[np.ndarray]) is correctly narrowed per call site -- a plain
Union return type was tried first and broke dofs= type-checking in 15
demos that pass a single FunctionSpace, since mypy can't tell from the
Union alone which branch a given call site takes.

That overload split then surfaced a second real bug: dirichletbc's
`dofs` parameter was typed as a single ndarray only, but its C++
constructor also has a Sequence[ndarray]-accepting overload used when
V is a sub-space and value's function space differs (e.g.
demo_matrix-free-petsc.py, passing the dof-index pair straight from
locate_dofs_topological((W.sub(0), V), ...)). Widened dofs to
npt.NDArray[np.int32] | Sequence[npt.NDArray[np.int32]] and corrected
the docstring accordingly.

The remaining 5 ignores in this file (DirichletBC.set, and the
dtype-dispatch construction in dirichletbc()) are the same
wrapper-stores-a-dtype-Union-then-dispatches-at-runtime pattern seen
throughout this codebase: bctype/`_value`/`self._cpp_object` are only
known to be a *consistent* concrete dtype at runtime (via the
cpp_types[dtype, geometry_dtype] lookup table), which mypy cannot
verify statically. A typing.cast here would have to pick one of four
concrete types with no static basis for which -- unlike the fixes
above, there is no sound local fix without restructuring the dispatch
mechanism itself, so these are left as targeted ignores.

Verified with ruff check/format, mypy (-p dolfinx, test, and demo, all
clean, matching CI's exact invocation), and pytest
(test/unit/fem/test_bcs.py, 24/24 passing).

* Fix two real bugs breaking CI: demo_mixed-topology.py and petsc.py contains()

demo_mixed-topology.py crashed on every CI run (confirmed identical on
unmodified main via git stash, so unrelated to this PR's own commits):
`dirichletbc(value=0.0, dofs=bcdofs, V=V_cpp)` passed a raw C++
FunctionSpace built from a raw C++ Mesh (both from the low-level
dolfinx.cpp.mesh.create_mesh binding this demo uses directly, since
UFL doesn't yet support mixed-topology domains). dirichletbc needs
V.mesh to have a real UFL domain to build the Constant for the
boundary value, but a raw cpp Mesh has no ufl_domain()/_ufl_is_terminal_.
Fixed by reusing the same Mesh(mesh, domain)/FunctionSpace(...) wrapping
idiom the file already uses later (line ~186) for form assembly --
picking one cell type's domain/element arbitrarily, since neither is
used for anything beyond this association.

fem/petsc.py's _assemble_matrix_petsc called
`row_forms[0].function_spaces[0].contains(bc.function_space)`, but
`.contains()`'s only overload takes a raw cpp FunctionSpace while
`bc.function_space` returns the Python wrapper -- a TypeError on every
block-assembled LinearProblem.solve() with a DirichletBC, breaking
demo_stokes.py's nested_iterative_solver_high_level and
demo_mixed-poisson.py in the PETSc-enabled CI matrix. Fixed by passing
bc.function_space._cpp_object instead. (This fix already existed
uncommitted in the worktree from earlier work -- committing it now
since it's exactly what these two failing demos need.)

Verified demo_mixed-topology.py runs to completion locally (prints
"Solution vector norm ...", no exceptions) plus a clean mypy/ruff pass.
petsc.py's fix verified against the exact CI traceback (same file,
same line, same call site in both demo_stokes.py and
demo_mixed-poisson.py); could not run it directly in this session's
non-PETSc local build, but the fix is unambiguous: .contains()'s sole
registered overload requires a raw cpp FunctionSpace_float64, which
._cpp_object provides and the bare wrapper does not.

---------

Co-authored-by: qbisicwate <qbisicwate@gmail.com>
Co-authored-by: schnellerhase <56360279+schnellerhase@users.noreply.github.com>
Co-authored-by: Jack S. Hale <mail@jackhale.co.uk>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
pull Bot pushed a commit to gnikit/dolfinx that referenced this pull request Aug 5, 2026
…S#4348)

* add cpp stub files (#4096)

* add cpp stub files

* fix nanobind OUTPUT path

Note that these OUTPUT are not actually passed to the stub
generator and purely used for dependency management
within CMake.

* Fix: reserved python global keyword

* Fix: non-install time for UNIX platforms + CI adaptation

* Fix: scoped log import

* fix: petsc...

* Work in progress on autogenerating nanobind stubs

* Generate dolfinx.cpp stubs automatically

* Add a way to disable nanobind stubgen (e.g. HPC builds)

* Fix formatting

* Fix on platforms without petsc4py

* Use spack-fenics due to updated base deps

* Back to main

* Fix.

* Revert

* Fix Windows module name

* Fix mypy failures surfaced by nanobind stub generation

The auto-generated dolfinx.cpp stubs made mypy see real, precise types
for the compiled extension for the first time, surfacing ~446 errors
in the "Build and test" CI job (the only mypy invocation that actually
installs dolfinx before running mypy, so the only one exercising the
generated stubs). Root causes and fixes:

- Unix stub generation imported the compiled module as bare `cpp`
  instead of `dolfinx.cpp`, so nanobind wrote cross-submodule
  references like `import cpp.la`, which doesn't exist and silently
  resolved to Any under mypy, masking real errors and producing bogus
  "overload can never match" diagnostics. Stage the built module under
  a throwaway dolfinx/ package dir before invoking nanobind_add_stub
  so the module's real __name__ is dolfinx.cpp.
- `_IntegralType`/`MPICommWrapper` bindings used names or const_name()
  values with no resolvable Python type, breaking stub cross-refs.
- `FiniteElement`/`AdjacencyList` `__eq__` bindings exposed the raw C++
  operator==, violating object.__eq__'s Liskov contract; wrapped in a
  lambda with an isinstance guard instead.
- Dead, unreachable overloads (complex instantiations of
  interpolation_matrix/discrete_curl/discrete_gradient that are always
  shadowed by the real ones, and a redundant read_geometry_data
  registration) removed.
- Missing nanobind/stl/map.h and .../string.h includes were causing
  stubgen to fall back to invalid raw C++ type strings.
- ~250 Python-side errors are the same runtime-safe-but-statically-
  unverifiable scalar-type dispatch pattern already handled elsewhere
  in this PR (fem/petsc.py); fixed with matching `# type: ignore`.
  A handful were real bugs instead: wrong return-type annotations in
  forms.py, and dispatch-table locals missing an explicit Union
  annotation.
- The ~24 remaining errors are genuine ecosystem-level gaps confirmed
  via isolated repros (mypy can't disambiguate numpy dtype/rank in
  NDArray annotations; nanobind has no std::reference_wrapper caster;
  basix's C++ types aren't stub-resolvable across the package
  boundary) rather than dolfinx bugs. Suppressed per-file via a new
  nanobind stub pattern file (stub_patterns.txt) that injects a
  scoped `# mypy: disable-error-code=...` into just the affected
  generated stubs.

Verified: mypy clean (matching the failing job's exact PETSc/ADIOS2
config), ruff/clang-format clean, and a second full build with
PETSc+SLEPc+ADIOS2+ParMETIS+SuperLU_DIST (-Werror) compiles cleanly
with runtime sanity checks passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Extend mypy type-ignore fixes for new/changed main-branch APIs

Post-merge fixups: main added a mesh argument to pack_coefficients,
an interpolate_geometry function, and reshaped a few overload call
sites (dofmaps as a sequence instead of a method, apply_lifting's
now-arg-type instead of call-overload mismatch). Same runtime-safe/
statically-unverifiable scalar-dispatch pattern as the rest of this
branch; verified mypy clean again afterwards.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Format python/CMakeLists.txt with gersemi

Fixes Lint CI failure introduced by the main merge, which brought in
the gersemi CMake formatter (replacing cmake-format). Purely cosmetic
reformatting of the nanobind stub-generation additions; no logic
changes.

* Fix mypy errors only visible in package-mode type checking

The "Build and test" CI job runs mypy in package mode
(`mypy -p dolfinx`) against a genuinely installed dolfinx with real
nanobind-generated stubs, unlike the Lint job's `mypy dolfinx`, which
never builds/installs dolfinx and so resolves dolfinx.cpp.* as Any via
ignore_missing_imports, masking overload-resolution errors entirely.
Package mode surfaced 34 real errors invisible in Lint mode:

- bcs.py/assemble.py: the DirichletBC/insert_diagonal overload
  mismatches report as call-overload in package mode, not the
  arg-type code the ignores were scoped to; switch to codeless
  ignores since the reported code is unstable across the two modes.
- io/utils.py: new arg-type error on write_function's getattr cast.
- mesh.py: refine()'s partitioner annotation didn't include None,
  even though the docstring documents None as a valid value and
  callers (test_refinement.py) pass it.
- test_mesh_partitioners.py: overly narrow inferred list element type
  broke when a ParameterSet skip-marker was appended.
- test_mesh.py: partitioner_kahip/partitioner_parmetis are only
  present on the compiled module when built with those optional
  backends, which mypy can't know statically.
- demo_mixed-topology.py, demo_static-condensation.py: same
  dtype-union-overload ambiguity pattern fixed elsewhere in this PR.

Verified via a from-scratch venv replicating the CI job exactly
(Python 3.12, non-editable install, real generated stubs): mypy passes
in all three invocation modes (Lint's mypy dolfinx/test/demo, and
package-mode -p dolfinx/test/demo), ruff check/format clean, and the
affected test files pass under pytest.

* Also ignore partitioner_scotch attr-defined in test_mesh.py

The CI runner for the "Build and test" job doesn't have libscotch
installed, so partitioner_scotch is absent from the compiled module
there too (unlike my local reproduction, which has SCOTCH via
Homebrew) -- same build-config-dependent attribute-existence issue as
partitioner_kahip/partitioner_parmetis fixed in the previous commit.

* Remove two avoidable mypy ignores

- fem/utils.py: narrow space0/space1's cpp objects via a match
  statement in interpolation_matrix so mypy can resolve the matching
  interpolation_matrix overload directly, instead of ignoring the
  call. Also gives callers a clear TypeError on mismatched dtypes
  instead of an opaque nanobind overload-resolution failure.
- io/gmsh.py: read_from_msh's partitioner parameter typed its
  callback's 4th argument as AdjacencyList (the pure-Python wrapper,
  unused elsewhere in this file) instead of _AdjacencyList_int32 (the
  cpp type that model_to_mesh, which it delegates to, actually
  expects). Fixing the annotation removes the genuine type mismatch
  instead of suppressing it.

* Switch VTKFile/XDMFFile to composition instead of subclassing cpp types

VTKFile and XDMFFile subclassed their nanobind-bound counterparts
directly (_cpp.io.VTKFile/_cpp.io.XDMFFile), unlike Mesh, Function,
FunctionSpace, and VTXWriter, which all wrap their cpp object via a
_cpp_object attribute instead. Subclassing meant every overridden
method that re-types its arguments from the raw cpp type to the
friendlier Python wrapper type (Mesh vs Mesh_float32/64, etc.) was a
genuine Liskov substitution violation from mypy's point of view,
requiring a # type: ignore[override] on write_mesh, write_meshtags,
write_function, and read_meshtags.

Switching to composition removes the inheritance relationship, so
these methods are no longer checked against a base signature and the
four ignores are gone with no suppression left behind. This requires
explicit delegation for every previously-inherited method actually
used elsewhere (close, comm, flush, write_information,
read_information, read_topology_data, read_geometry_data,
read_cell_type), confirmed via a grep across tests and demos, plus
write_geometry for API parity even though nothing in-tree calls it.

read_meshtags's and the new write_geometry's underlying cpp functions
only support float64 meshes/geometry; narrowed via isinstance instead
of suppressing, giving a real static check and a clear TypeError for
the unsupported case instead of an ignore.

* Remove 3 more avoidable mypy ignores in io/vtkhdf.py

- read_mesh's filename was passed straight through as str | Path, but
  the cpp read_vtkhdf_mesh_float32/64 bindings only accept str; cast
  explicitly instead of ignoring, narrowing the float32 branch's
  ignore down to just the pre-existing, unrelated assignment error.
- variant (mesh_cpp.geometry.cmaps[0].variant) is a raw int from the
  cpp binding; basix.ufl.element expects a LagrangeVariant. Wrap it,
  matching the same conversion already used in fem/utils.py's
  interpolate_geometry.
- cell_types[0].name's ignore was stale: removing it produces no
  error, confirmed by rebuilding and rerunning mypy.

Found via a full audit: stripped every remaining arg-type ignore in
the tree at once, rebuilt against a from-scratch venv mirroring CI's
package-mode mypy check, and inspected every resulting error. Outside
of this file, everything else needs its ignore back - each is a
single or multiple _cpp_object-typed argument whose Python-level type
is a plain dtype union unconnected to any generic parameter (same
pattern as FunctionSpace/Mesh/Form discussed elsewhere), so narrowing
would mean adding an isinstance/match branch per dtype for no real
safety benefit. Left those as ignores rather than trade a suppression
for genuine complexity.

* Remove remaining ignore[assignment] in vtkhdf.read_mesh

mesh_cpp's type was inferred from its first assignment
(read_vtkhdf_mesh_float64 -> Mesh_float64), so the second branch's
Mesh_float32 assignment was flagged as incompatible. Declare the
union type explicitly up front, matching the same pattern already
used for the analogous float32/float64 dispatch in mesh.py's
create_interval/create_rectangle/create_box.

* Remove 5 stale arg-type ignores

Found via a systematic strip-and-rebuild audit of every remaining
ignore[arg-type] comment: these 5 produce no mypy error at all once
removed, in package mode, mypy test, and mypy demo. No code changes
beyond deleting the comments.

* Fix wrong partitioner Callable signature in gmsh.py

model_to_mesh's and read_from_msh's partitioner parameter was typed
as Callable[[Comm, int, int, AdjacencyList_int32], AdjacencyList_int32],
but that value is passed straight into create_mesh, whose cpp binding
requires Callable[[Comm, int, Sequence[CellType],
Sequence[NDArray[int64]]], AdjacencyList_int32] - matching what
dolfinx.mesh.create_cell_partitioner's return type already documents.
The 3rd/4th argument types were simply wrong, not an inherent
dtype-overload ambiguity.

* Remove 2 arg-type ignores in geometry.bb_tree

Branch on isinstance(mesh._cpp_object, Mesh_float32/64) instead of
np.issubdtype(mesh.geometry.x.dtype, ...) - same if/elif structure,
but discriminating on the actual value being passed to the cpp
constructor lets mypy narrow it through each branch.

* Remove arg-type ignore in mesh.create_geometry

The function already validates that element.dtype matches x.dtype
before construction, so element._cpp_object's own concrete type
already determines which Geometry class to build - dispatch off
isinstance(cpp_element, ...) directly instead of a separate ftype
lookup keyed on x.dtype. Removes a layer of indirection rather than
adding one.

Incidental fix: the "Unknown floating type for geometry" message was
missing its f-string prefix, so {x.dtype} was never interpolated.

* Remove 2 arg-type ignores in mesh.create_point_mesh

geometry was just constructed from points.dtype a few lines above, so
geometry._cpp_object's own concrete type already determines which
Mesh class to build; branch on isinstance(cpp_geometry, ...) instead
of points.dtype == ... for the same reason as the bb_tree/
create_geometry fixes.

* Remove arg-type ignore in mesh.create_cell_partitioner

@singledispatch requires the base function's part parameter to be
typed Callable | GhostMode (a supertype of the registered GhostMode
variant), but the base implementation is only ever reached when part
is genuinely a Callable - a GhostMode argument gets routed to the
registered variant instead. Add a defensive isinstance guard so mypy
narrows part to Callable for the rest of the function, matching what
was already true at runtime.

* Remove 2 arg-type ignores in graph.comm_graph_data/comm_to_json

Both are non-overloaded cpp functions accepting only
AdjacencyList_int_sizet_int8__int32_int32, and the only Python-level
producer of that type is comm_graph() (confirmed via the cpp stub:
its return type is unconditionally that one class). Add an isinstance
guard, giving callers a clear error instead of an opaque nanobind
overload failure if they pass an AdjacencyList from anywhere else.

* Remove arg-type ignore in LinearProblem's preconditioner form

form()'s parameter was typed ufl.Form | Sequence[ufl.Form] |
Sequence[Sequence[ufl.Form]] with no None, even though
LinearProblem's P (the preconditioner) is documented and typed as
optional, and _create_form's implementation already passes None
straight through unchanged via its final `else: return form` branch.
Widen the annotation to match what the implementation already does.

* Remove arg-type ignore in derivative_block's rank-one Jacobian branch

The rank-zero branch already guards u with isinstance(u, Function) /
isinstance(u, Sequence), raising a clear ValueError otherwise. The
rank-one branch called _derive_univariate_jacobian(F, u, du) (which
requires u: Function, du: ufl.Argument | None) with no equivalent
guard, so a caller passing u or du as a sequence would previously fall
through silently instead of getting the same clear error the sibling
branch gives.

* Remove arg-type ignore in LinearProblem.solve

mypy type-checks functools.singledispatch calls only against the base
function's signature (L: typing.Any, constants: npt.NDArray | None,
...), so passing self.L (a Form) as the second positional argument to
assemble_vector(self.b.array, self.L) was checked against
constants: npt.NDArray | None and flagged. self.b.array/self.L are
concretely npt.NDArray/Form here (not ambiguous unions), so calling
the registered variant _assemble_vector_array directly - the exact
function singledispatch would have dispatched to anyway - resolves
cleanly. Same pattern already used in fem/petsc.py for the same
functools.singledispatch/mypy limitation.

* Remove 6 arg-type ignores in fem/petsc.py assemble_vector/assemble_matrix

functools.singledispatch base functions recursively called their own
singledispatch wrapper to reach the registered PETSc.Vec/PETSc.Mat
variant, but mypy only checks singledispatch calls against the base
signature - which is shaped for the "no b/A supplied yet" case, not
the (b, L, ...)/(A, a, ...) shape actually being passed. Name the two
previously-anonymous registered variants (_assemble_vector_petsc,
_assemble_matrix_petsc) and call them directly at each of the 6 call
sites (the base function's own recursive call, the NEST sub-block
recursion, and 4 call sites in LinearProblem/assemble_residual/
NewtonSolverNonlinearProblem), bypassing the mismatched base check
entirely.

Also fixes a genuine pre-existing bug: the base assemble_matrix's
constants/coeffs parameter types didn't match its own
a: Form | Sequence[Sequence[Form]] shape (Sequence[X]/Sequence[dict]
instead of bare X/Sequence[Sequence[dict]]), inconsistent with the
correctly-typed registered sibling - this was the source of 2 of the
6 errors surviving the rename alone.

Adds one small isinstance(coeffs, dict) guard in the NEST-matrix
branch, mirroring the existing isinstance(a, Sequence) check right
above it, for a genuinely-reachable misuse case.

Verified against a from-scratch PETSc-enabled build of this worktree
(petsc4py isn't available in the mypy-checking venv used elsewhere in
this branch's history): assemble_vector/assemble_matrix in both their
new-object and existing-object forms, LinearProblem.solve(),
assemble_residual, NewtonSolverNonlinearProblem.J, NEST-matrix
assembly, and the new guard's error path.

* Remove 3 more arg-type ignores in fem/petsc.py assemble_vector

Same root cause as assemble_matrix's NEST branch: constants/coeffs
still carry their full declared union type (including shapes meant
for the other branches) at each _assemble_vector_array call site,
since narrowing L doesn't narrow the separate constants/coeffs
parameters. Add isinstance guards mirroring the existing
isinstance(L, Sequence) checks - a bare dict in the NEST/block
branches, or a Sequence in the single-form branch, is a genuine
caller error these now catch explicitly instead of silently
misbehaving.

Verified against a from-scratch PETSc-enabled build: NEST vector
assembly, block-offset vector assembly, single-form assembly, and
both new guards' error paths.

* Remove arg-type ignore in set_bc's NEST recursion

The outer isinstance(bcs[0], Sequence) check establishes that bcs is
genuinely 2D before reaching the NEST branch, but mypy can't propagate
a check on an element (bcs[0]) to narrow bcs's own declared type. Add
a per-iteration isinstance(bc, Sequence) guard instead, which mypy can
use to narrow bc directly for the recursive set_bc call.

Verified against a from-scratch PETSc-enabled build: NEST set_bc with
correctly-nested bcs, and the new guard's error path with malformed
input.

* Remove 5 arg-type ignores in LinearProblem/assemble_residual block paths

Two genuine pre-existing bugs surfaced once investigated:

- LinearProblem.a/.preconditioner properties were typed
  Form | Sequence[Form], but __init__ actually accepts and stores
  Form | Sequence[Sequence[Form]] (a: ufl.Form | Sequence[Sequence[
  ufl.Form]]), matching the class docstring's a_ij(u, v) block-matrix
  description. L is correctly 1D as declared.

- extract_function_spaces's third @typing.overload declared
  -> list[list[FunctionSpace | None]] for 2D forms, but the
  implementation's 2D branch returns list(unique_spaces(V)) - a flat
  list, same shape as the second overload. Confirmed against the
  existing test_extract_function_spaces test, which indexes the
  result with Vc[0]/Vc[1], not Vc[0][0].

With both fixed, 3 call sites still needed a small isinstance guard
(LinearProblem's block and single-form branches, assemble_residual's
block branch), since self.a/self.L/residual are properties/parameters
that can't be narrowed by checking a different variable
(self.u/jacobian) - same idiom as the earlier set_bc/assemble_matrix
fixes.

Verified against a from-scratch PETSc-enabled build: a well-posed
block LinearProblem.solve() (correct solution norms, exact Dirichlet
BC enforcement), assemble_residual's block path, and the new guards'
error paths.

* Remove 16 type-ignore comments in fem/function.py

10 were stale - removing them produces no mypy error at all, in any
of the three checked modes.

The other 6 (in functionspace()) were caused by a real bug: the
function reused its own element parameter (typed
AbstractFiniteElement | ElementMetaData | tuple[...]) to hold the
result of finiteelement(...), a completely different type (the
compiled FiniteElement wrapper). Reassigning a parameter to an
incompatible type confuses mypy's flow analysis for every subsequent
use, not just the reassignment itself. Renamed to dolfinx_element.

Two remaining ignores in the same function (689, 699) are left alone:
a try/except TypeError duck-typing fallback that genuinely can't be
narrowed without restructuring the ElementMetaData conversion.

Verified: mypy clean in all three modes, ruff clean,
test_function.py/test_custom_basix_element.py pass (74 tests), and a
direct runtime check of functionspace() for both float32 and float64.

* Remove 16 type-ignore comments in mesh.py/fem/element.py, fix h()'s hardcoded dtype

9 were stale - removing them produces no mypy error in any of the
three checked modes.

4 more (coordinate_element's singledispatch base-vs-registered
mismatch, same limitation as assemble_vector/assemble_matrix fixed
earlier in petsc.py): named the anonymous registered variant
_coordinate_element_from_basix and called it directly at all 4
call sites in mesh.py.

3 more, from two real bugs in refine()/uniform_refine():
- refine()'s return type omitted | None for parent_cell/parent_facet
  even though its own docstring says "(optional) parent cells,
  (optional) parent facets" and the underlying cpp function genuinely
  returns NDArray | None for both.
- both functions accessed msh._ufl_domain.ufl_coordinate_element()
  without checking _ufl_domain (itself typed ufl.Mesh | None) isn't
  None first. Added explicit guards with a clear ValueError instead of
  a potential silent AttributeError.

Also fixed (but did not remove the ignore for) Mesh.h()'s return type,
hardcoded to npt.NDArray[np.float64] even though the underlying
_cpp.mesh.h is genuinely overloaded per dtype and Mesh is
Generic[Real] - changed to npt.NDArray[Real]. The ignore stays since
_cpp_object's type still isn't tied to Real (same architectural gap as
Geometry.x), but the annotation is now honest about float32 meshes.

Verified: mypy clean in all three modes, ruff clean, 433 tests pass,
and direct runtime checks of h() for both dtypes, normal
refine/uniform_refine, both new guards' error paths, and both
create_mesh code paths using the renamed function.

* Remove 4 arg-type ignores in fem/utils.py via match-based narrowing

create_interpolation_data, discrete_curl, discrete_gradient, and
interpolate_geometry each take two or three independent FunctionSpace/
Mesh/Geometry/CoordinateElement/FiniteElement objects that must share
a dtype for the underlying cpp overload to resolve - same shape as
interpolation_matrix, fixed earlier with the same technique. Narrow
via match/isinstance instead of ignoring, so a genuine dtype mismatch
between the objects now raises a clear TypeError instead of an opaque
nanobind overload failure.

Verified: mypy clean in all three modes, ruff clean, 121 PETSc tests
pass (test_petsc_discrete_operators.py, against a from-scratch
PETSc-enabled build) plus 215 (test_interpolation.py) + 28
(test_interpolate_geometry.py) non-PETSc tests.

* Remove 4 stale type-ignore comments in fem/bcs.py

These 4 (the non-Iterable-V early-return branches and the block-form
_V list comprehensions in locate_dofs_geometrical/locate_dofs_
topological) produce no mypy error at all once removed, in any of the
three checked modes.

The remaining 10 ignores in this file are genuine: DirichletBC.g/
function_space return the raw cpp object instead of the declared
wrapped Function/Constant/FunctionSpace type (g even has its own
"TODO: needs to be wrapped" comment - a known, deliberately-deferred
gap, not something to silently implement here), and dirichletbc()'s
_value/bctype correlation is intentional polymorphism across raw
arrays, Function, Constant, and scalar values, not a variable-reuse
bug.

Verified: mypy clean in all three modes, ruff clean, test_bcs.py
passes (24 tests), and a direct runtime check of both freed
block-form call paths.

* Remove more type: ignore comments in fem/forms.py

- extract_function_spaces: remove stale union-attr ignore (forms is
  already narrowed at this point).
- compile_form: replace assignment ignore with an explicit
  typing.cast, since ffcx.get_options() returns a heterogeneous dict.
- derivative_block: extend the isinstance-based du/u narrowing already
  used in the rank-one branch to the rank-zero and block-Jacobian
  branches, removing the three remaining bare ignores.

* Fix singledispatch call-arg bug in create_cell_partitioner call sites

The @create_cell_partitioner.register(GhostMode) variant was anonymous
(named _), so mypy checked its call sites against the 3-argument base
function's signature instead of the actual 2-argument dispatched
overload, producing a bogus "Missing positional argument" error at
every call site. Name the registered function and call it directly at
the two internal call sites (mesh.create_mesh, XDMFFile.read_mesh),
removing both ignores. Also drop an inert '# F401' comment left over
on the VTXMeshPolicy import (ruff confirms the import is used).

* Fix same create_cell_partitioner call-arg bug in demo_mixed-topology.py

Same root cause as the previous mesh.py/io/utils.py fix: call the named
GhostMode-dispatch variant directly instead of through the generic
singledispatch function, whose base signature mypy incorrectly checks
call sites against.

* Fix same create_cell_partitioner call-arg bug in demo_axis.py/demo_pml.py

Same root cause and fix as the previous two commits: call the named
GhostMode-dispatch variant directly instead of through the generic
singledispatch function.

* Remove stale type: ignore in plot.py

Once the @overload/@singledispatch attr-defined error on vtk_mesh.register
fires, mypy no longer independently checks the registered function body,
making its own ignore comment redundant.

* Replace var-annotated ignores with explicit type hints in demo_tnt-elements.py

Empty-list literals can't have their element type inferred by mypy;
annotate x/M as list[list[np.ndarray]] instead of suppressing.

* Guard against None function space in NewtonSolver.__init__

extract_function_spaces(problem.L) is statically Optional; add an
explicit None check before calling create_vector, which requires a
non-optional FunctionSpace for its single-space overload.

* Correct type: ignore error codes in VTXWriter for ADIOS2-enabled builds

Verified against a real ADIOS2+petsc4py build: the previous ignore
codes (attr-defined only, union-attr) were only correct for the
no-ADIOS2 build and silently did nothing once ADIOS2 attributes
genuinely exist. Add the assignment/arg-type codes that actually fire
in an ADIOS2-enabled build, and add a missing ignore on the
Function-sequence VTXWriter constructor call.

* Remove stale type: ignore comments in la/petsc.py

Verified against a real petsc4py build (by patching a locally-generated
stub defect that was blocking mypy analysis entirely -- not part of
this diff): PETSc is always a real, unconditionally-imported module in
this file (guarded only by a runtime RuntimeError, never
TYPE_CHECKING), so none of the name-defined/attr-defined ignores were
ever needed. Only createGhostWithArray/createGhost's argument
type mismatches and one singledispatch dispatch-type mismatch are
genuine; corrected their codes and line placement to match where mypy
actually reports them.

* Correct type: ignore comments in nls/petsc.py

Verified against a real petsc4py build: the A/b property ignores were
stale (PETSc.Mat/Vec are real types here). solve/setP genuinely violate
the Liskov substitution principle against the cpp base class's Vec/Mat
signatures by design (the Python wrapper takes Function/high-level
callables); give them the specific override code instead of a bare
ignore.

* Correct type: ignore comments in fem/petsc.py, fix singledispatch bugs

Verified against a real petsc4py+ADIOS2 build (by patching a locally
generated nanobind stub defect that was blocking mypy analysis
entirely -- not part of this diff) and the no-petsc4py build:

- 74 of 150 ignores were stale: PETSc.Vec/Mat/etc. are always real
  types here (petsc4py is unconditionally imported, never
  TYPE_CHECKING-gated), so the name-defined/attr-defined ignores from
  when this wasn't reliably checkable no longer apply.
- 10 ignores had the wrong error code and were silently doing nothing.
- 20 lines were missing ignores for errors that leaked through
  undetected.
- Fixed 3 real call sites (LinearProblem.solve, assemble_jacobian,
  NewtonSolverNonlinearProblem.F) that called the generic
  assemble_matrix/assemble_vector singledispatch functions positionally
  plus a bcs= keyword, which mypy correctly flags as a keyword
  conflict against the singledispatch base signature (Python's
  singledispatch itself dispatches fine at runtime, but mypy only
  checks calls against the un-registered base signature). Call
  _assemble_matrix_petsc/_assemble_vector_petsc directly instead,
  matching the pattern already used elsewhere in this file.

Verified with the full PETSc-marked pytest suite (174 passed) plus
direct runtime smoke tests of assemble_matrix, assemble_vector,
apply_lifting, LinearProblem.solve, and discrete_gradient.

* Fix type: ignore comments in demo_stokes.py

Verified against a real petsc4py build: numpy.dtype (PETSc.ScalarType's
declared stub type) is a valid DTypeLike, so np.zeros(..., dtype=...)
needed no ignore. Calling PETSc.ScalarType(0) as a constructor does
genuinely error against that same stub type; give it the operator code.

* Remove stale type: ignore comments in demo_matrix-free-petsc.py

Verified against a real petsc4py build: these zip() unpackings type
check cleanly once petsc4py's real stubs are available.

* Fix type: ignore comments in demo_static-condensation.py

Verified against a real petsc4py build: 12 of 15 ignores were stale.
bc.set(b) was missing an ignore -- DirichletBC.set expects an ndarray,
not the PETSc.Vec passed here.

* Fix type: ignore comments for jv() calls in EM scattering demos

Verified against a real petsc4py build: jv(nu, alpha) with a real alpha
type-checks fine; only jv(nu, m * alpha), where m is complex, needed an
ignore, with the call-overload code.

* Fix type: ignore comments in several demos

Verified against a real petsc4py build: the ScalarType import, PETSc.Sys()
and PETSc.Error except-clause, and float32-check ignores were all stale.
demo_pyamg.py's dirichletbc(value=dtype(0.0), ...) call genuinely errors
against a runtime-constructed dtype; give it the specific operator/misc
codes instead of a bare ignore.

* Fix type: ignore comments in demo_axis.py

Verified against a real petsc4py build: the complexfloating check was
stale. sys = PETSc.Sys()/hasExternalPackage genuinely error against the
petsc4py.PETSc module-vs-Sys-class stub; give them the specific
assignment/attr-defined codes.

* Remove stale type: ignore comments in demo_gmsh.py/demo_interpolation-io.py

ignore_missing_imports = true is a global [tool.mypy] setting shared by
every CI job's pyproject.toml, so an unstubbed import (gmsh) or an
attribute access on an object derived from one (pyvista's Plotter)
can never actually error under this config.

* Fix singledispatch call-arg bug in fem/problems.py LinearProblem.solve

Same root cause as the fem/petsc.py/mesh.py fixes: call the named
MatrixCSR-dispatch variant (_assemble_matrix_csr) directly instead of
through the generic singledispatch function, whose base signature mypy
incorrectly checks call sites against.

* Fix nanobind stub type names for PETSc Mat/Vec/IS/KSP casters

PETSC_CASTER_MACRO used bare identifiers (mat, vec, is, ksp) as the
nanobind stub type name, instead of fully-qualified petsc4py.PETSc.*
names like caster_mpi.h correctly does for mpi4py.MPI.Comm. This
produced invalid generated stubs everywhere these types appear
(la.petsc, fem.petsc, nls.petsc) -- including a literal Python syntax
error, since `is` is a keyword, that crashes mypy outright when
checking against a real PETSc-enabled build's stubs. This is almost
certainly why so much PETSc-touching code accumulated broad
`# type: ignore` comments: mypy against these types was never reliably
checkable to begin with.

Verified by rebuilding the nanobind extension and regenerating stubs
directly with nanobind.stubgen: la.petsc/fem.petsc/nls.petsc now emit
valid, correctly-qualified types with the petsc4py.PETSc import
auto-added. Full PETSc-marked pytest suite passes (174 tests).

* Fix two more nanobind stub type leaks in la.cpp and io.h

SparsityPattern's "concatenate sub-patterns" constructor took maps as a
raw std::reference_wrapper<const IndexMap> directly in the nanobind-
facing signature; nanobind has no caster that unwraps reference_wrapper
to its underlying (already-bound) type, so the stub fell back to a raw,
invalid C++ type-name string. Fixed following the pattern already used
for DirichletBC elsewhere (assemble.h): accept shared_ptr<const
IndexMap> at the binding boundary (which nanobind resolves natively)
and build the reference_wrapper internally before forwarding to the
real constructor. This was the sole cause of the
dolfinx.cpp.la.__prefix__ valid-type suppression in stub_patterns.txt,
confirmed by regenerating the stub and running mypy with the
suppression removed -- now deleted.

VTXWriter's Function-list constructor accepts all four scalar/geometry
combinations at the C++ level (matching the real, intentional
ADIOS2Writers.h API), but only the two matched-precision combinations
per geometry type are ever bound to a Python fem.Function class, so
the other two are Python-unreachable yet still leaked into the stub as
unresolvable raw type names. Give the two per-T overloads an nb::sig
override restricting the declared type to what's actually reachable;
the C++ overload itself is unchanged.

Verified by rebuilding, regenerating stubs directly via
nanobind.stubgen, confirming mypy -p dolfinx is clean, and running the
complete python/test suite (3108 passed, 92 skipped, 27 xfailed,
matching the pre-change baseline).

* Simplify nanobind stub generation in python/CMakeLists.txt

Hoist install(TARGETS cpp ...) out of the ENABLE_NANOBIND_STUBGEN
branches so the compiled module is always installed, even with stub
generation disabled. Factor the duplicated 17-entry .pyi OUTPUT list
into a single NANOBIND_STUB_OUTPUTS variable shared by the WIN32 and
UNIX nanobind_add_stub() calls.

* Always run nanobind stub generation, remove ENABLE_NANOBIND_STUBGEN option

Stub generation is not opt-out in practice (no CI job or packaging
path disables it), so the option only added an untested configuration
path. Run it unconditionally instead.

* Link nanobind_add_stub docs from the UNIX stub-staging comment

Points readers at the mechanism (stubgen imports MODULE and infers
output location from __file__) that motivates staging cpp under a
throwaway dolfinx/ package directory.

* Drop the symlink add_custom_command for UNIX stub staging

Instead of building cpp normally and symlinking it into a throwaway
dolfinx/ directory post-build, set the cpp target's
LIBRARY_OUTPUT_DIRECTORY to build directly into that directory.
install(TARGETS cpp ...) still locates the target correctly regardless
of its output directory, so nothing else needs to change.

Verified with a from-scratch build: cpp links directly into
dolfinx/cpp.<ext>, stub generation produces the same
dolfinx.cpp.<submodule>-style cross-references as before, and
`cmake --install` places the .so under dolfinx/ as expected.

* Use a regular (non-editable) install in the RHEL/Spack CI job

The "AlmaLinux build and test" job has been failing deterministically
on every run since the nanobind stub-generation work landed: every
demo fails at import time with "ImportError: cannot import name 'cpp'
from partially initialized module 'dolfinx' (most likely due to a
circular import)".

This job is the only CI job that installs dolfinx with `pip install
-e` (editable). All non-editable installs across the rest of CI (the
PETSc-enabled matrix in ccpp.yml, plus repeated local reproduction
with an editable-install of this exact branch) succeed reliably.

The new stub generation puts `dolfinx/cpp/*.pyi` (a directory of type
stubs, matching nanobind's own convention for a compiled extension
with nested submodules) directly alongside the compiled
`dolfinx/cpp.<ext>` module. scikit-build-core's editable-install
redirect builds a manifest that classifies each installed path as
either a "wheel file" (compiled/source module) or a namespace-package
search location; a `.pyi`-only directory that exactly shadows a
compiled module's own name is an edge case scikit-build-core's own
source comments show has caused prior classification bugs in this
exact area (upstream issues #1427, #1482). This is a good fit for what
we observe: the module resolves fine via the ordinary installed-path
loader, but not through the editable redirect on this platform.

This CI job doesn't need editable mode -- it builds once and
immediately runs demos/tests against that one build, with no
edit-and-rerun step in between -- so switching to a regular install
sidesteps the redirect entirely rather than chasing the exact
upstream classification bug.

* Fix editable installs by requiring scikit-build-core>=1.0.0

Root-cause fix, replacing the earlier non-editable CI workaround
(previous commit): editable installs were never actually broken by
this PR's own code, but by a real bug in scikit-build-core <1.0.0's
editable redirect finder.

Empirically bisected locally (macOS, reproduced 100% on 0.11.0 through
0.12.2, 0/10 failures from 1.0.0 onward): the pre-1.0 redirect finder
resolves a compiled module straight from its known file path via
importlib.util.spec_from_file_location, without checking what else is
on disk. nanobind's generated dolfinx/cpp/*.pyi stub directory (its
standard convention for a compiled extension with nested submodules)
sits right next to the compiled dolfinx/cpp.<ext> module, and the
pre-1.0 build-time manifest scan registers dolfinx.cpp both as a
"wheel file" (the .so) and, from the stub directory's __init__.pyi, as
a package with its own search location -- confusing every subsequent
`from dolfinx import cpp` in dolfinx/common.py. 1.0.0 resolves compiled
modules through PathFinder instead, which correctly prefers the real
file over the same-named stub directory regardless of the manifest
ambiguity.

Since this is a real upstream fix rather than a workaround, restore the
RHEL/Spack CI job's editable install. That job's pinned Spack package
repo only provides py-scikit-build-core up to 0.12.2 (confirmed by
checking out the exact packages_ref tag), so pip-upgrade scikit-build-core
to >=1.0.0 from PyPI specifically for that build step rather than
relying on the Spack-provided one.

* Fix two mypy ignores caused by name reuse across incompatible types

demo_axis.py reused the module-level `sys` (the stdlib module, used for
sys.argv) as a local PETSc.Sys() instance; mypy forbids narrowing a
name to an incompatible type within the same scope. Renamed to
petsc_sys, which needs no suppression at all.

fem/assemble.py's _assemble_matrix_csr had the same pattern on the
`bcs` parameter, reassigning it from Sequence[DirichletBC] | None to a
list of raw _cpp_object handles. Renamed to _bcs (matching the
existing convention in fem/petsc.py), which resolves the [misc]
redefinition error. The [arg-type] ignore on the following
_cpp.fem.assemble_matrix call stays -- confirmed via mypy that it
suppresses three separate, genuine dtype-Union-vs-concrete-overload
mismatches unrelated to the renaming.

Verified with ruff check/format and a targeted mypy run against the
built stubs: demo_axis.py now has zero errors, assemble.py's remaining
ignore is the minimal one needed.

* Fix genuinely-fixable mypy ignores in PETSc/scipy demos

- demo_pyamg.py: narrow poisson_problem's dtype parameter from
  npt.DTypeLike to type[np.floating] | type[np.complexfloating],
  matching how it's actually called. This also exposed that
  dirichletbc's own value type hint was too narrow -- it already
  handles anything with a .dtype attribute at runtime, just didn't
  declare it -- so widen fem/bcs.py's dirichletbc signature to include
  raw numpy scalars instead of reaching for a lossy .item() conversion
  (which would have silently upcast float32 boundary values to
  float64).
- demo_pml.py / demo_scattering-boundary-conditions.py: scipy-stubs
  does support complex arguments to jv, just typed as numpy.complex128
  /complex64, not builtin complex -- wrap m * alpha accordingly. This
  uncovered a real bug: compute_a was annotated -> float but always
  returns a genuinely complex Mie coefficient (callers already take
  np.real/np.abs of it) -- fixed to -> complex in both files.
- demo_mixed-topology.py: cast hexahedron/prism's _cpp_object to
  CoordinateElement_float64 (both are built with the default
  dtype=np.float64, so this matches runtime reality) instead of
  ignoring the dtype-Union mismatch. This gives create_mesh's return
  type real precision, which surfaced two more pre-existing errors
  further down the same file that were previously masked by the
  broken overload match; added targeted ignores for those (same
  wrapper-Union-vs-concrete-overload pattern as elsewhere, no clean
  local fix available).
- assemble.py: insert_diagonal was still passed the stale `bcs` name
  after the earlier _bcs rename, a runtime bug (TypeError) hidden by
  a bare `# type: ignore`; fixed to reference _bcs, with the ignore
  narrowed to [call-overload] to match the actual error code.

Verified with ruff check/format, mypy against the built stubs, and by
actually running demo_pyamg.py (all four dtypes, correct precision
preserved) and demo_mixed-topology.py (runs through everything touched
here; its pre-existing failure further on, unrelated to this change,
reproduces identically on unmodified main).

* Clarify DirichletBC::set docs on ghost/owned-only x and x0 length

Neither the Python nor the C++ doc comment previously explained why
passing x with or without ghost entries changes what set() does. Traced
the mechanism in DirichletBC.h's apply() lambda: _dofs0 always contains
both owned and ghost dof indices, and the per-entry bounds check
`_dofs0[i] < x.size()` is what makes an owned-only x safe (ghost
indices are simply skipped) as well as a full local+ghost x (both get
set). Also document that x0, when provided, must be at least as long
as x -- only checked via assert in Debug/Developer builds, not a
per-element bounds check like x itself.

* Fix Group C: use PETSc.Vec.array_w instead of widening DirichletBC.set

demo_static-condensation.py was the only demo passing a raw PETSc.Vec
directly to DirichletBC.set(), which only works at runtime because
nanobind's ndarray caster happens to accept anything satisfying the
buffer protocol -- but statically needs an npt.NDArray, and PETSc.Vec
isn't typed as satisfying that anywhere. Every other demo doing the
identical assemble_vector/apply_lifting/ghostUpdate/set sequence
(demo_elasticity.py, demo_stokes.py) already uses b.array_w for
exactly this call; demo_static-condensation.py had just missed it.

Widening fem/bcs.py's DirichletBC.set signature to accept a buffer-like
type was considered and rejected: the underlying nanobind binding's own
generated stub types x as a concrete ndarray[float64, ...] regardless,
so a Python-level widening would only relocate the mismatch rather than
resolve it, and would require either a hard petsc4py dependency (which
fem/bcs.py deliberately avoids) or a buffer-protocol Protocol requiring
Python's 3.12+ collections.abc.Buffer (project floor is 3.11).

Verified with ruff check/format and mypy against the built stubs; b
remains the same PETSc.Vec object afterward (array_w is a zero-copy
view), so the later solver.solve(b, ...) call is unaffected.

* Fix two real bugs found while reviewing bcs.py's type: ignore comments

locate_dofs_geometrical/locate_dofs_topological's docstrings claimed
that passing an iterable of function spaces returns "a 2-D array of
shape (number of dofs, 2)". This is wrong: both the C++
implementation (std::array<std::vector<int32_t>, 2>) and every actual
call site (test_bcs.py's dofs[0]/dofs[1] indexing) treat it as a list
of one array per space. Fixed the docstrings, and split each function
into @overload declarations so the return type (np.ndarray vs.
list[np.ndarray]) is correctly narrowed per call site -- a plain
Union return type was tried first and broke dofs= type-checking in 15
demos that pass a single FunctionSpace, since mypy can't tell from the
Union alone which branch a given call site takes.

That overload split then surfaced a second real bug: dirichletbc's
`dofs` parameter was typed as a single ndarray only, but its C++
constructor also has a Sequence[ndarray]-accepting overload used when
V is a sub-space and value's function space differs (e.g.
demo_matrix-free-petsc.py, passing the dof-index pair straight from
locate_dofs_topological((W.sub(0), V), ...)). Widened dofs to
npt.NDArray[np.int32] | Sequence[npt.NDArray[np.int32]] and corrected
the docstring accordingly.

The remaining 5 ignores in this file (DirichletBC.set, and the
dtype-dispatch construction in dirichletbc()) are the same
wrapper-stores-a-dtype-Union-then-dispatches-at-runtime pattern seen
throughout this codebase: bctype/`_value`/`self._cpp_object` are only
known to be a *consistent* concrete dtype at runtime (via the
cpp_types[dtype, geometry_dtype] lookup table), which mypy cannot
verify statically. A typing.cast here would have to pick one of four
concrete types with no static basis for which -- unlike the fixes
above, there is no sound local fix without restructuring the dispatch
mechanism itself, so these are left as targeted ignores.

Verified with ruff check/format, mypy (-p dolfinx, test, and demo, all
clean, matching CI's exact invocation), and pytest
(test/unit/fem/test_bcs.py, 24/24 passing).

* Fix two real bugs breaking CI: demo_mixed-topology.py and petsc.py contains()

demo_mixed-topology.py crashed on every CI run (confirmed identical on
unmodified main via git stash, so unrelated to this PR's own commits):
`dirichletbc(value=0.0, dofs=bcdofs, V=V_cpp)` passed a raw C++
FunctionSpace built from a raw C++ Mesh (both from the low-level
dolfinx.cpp.mesh.create_mesh binding this demo uses directly, since
UFL doesn't yet support mixed-topology domains). dirichletbc needs
V.mesh to have a real UFL domain to build the Constant for the
boundary value, but a raw cpp Mesh has no ufl_domain()/_ufl_is_terminal_.
Fixed by reusing the same Mesh(mesh, domain)/FunctionSpace(...) wrapping
idiom the file already uses later (line ~186) for form assembly --
picking one cell type's domain/element arbitrarily, since neither is
used for anything beyond this association.

fem/petsc.py's _assemble_matrix_petsc called
`row_forms[0].function_spaces[0].contains(bc.function_space)`, but
`.contains()`'s only overload takes a raw cpp FunctionSpace while
`bc.function_space` returns the Python wrapper -- a TypeError on every
block-assembled LinearProblem.solve() with a DirichletBC, breaking
demo_stokes.py's nested_iterative_solver_high_level and
demo_mixed-poisson.py in the PETSc-enabled CI matrix. Fixed by passing
bc.function_space._cpp_object instead. (This fix already existed
uncommitted in the worktree from earlier work -- committing it now
since it's exactly what these two failing demos need.)

Verified demo_mixed-topology.py runs to completion locally (prints
"Solution vector norm ...", no exceptions) plus a clean mypy/ruff pass.
petsc.py's fix verified against the exact CI traceback (same file,
same line, same call site in both demo_stokes.py and
demo_mixed-poisson.py); could not run it directly in this session's
non-PETSc local build, but the fix is unambiguous: .contains()'s sole
registered overload requires a raw cpp FunctionSpace_float64, which
._cpp_object provides and the bare wrapper does not.

* Use typing.overload to give shape-dependent return types real precision

fem/forms.py:
- form(): had no return type annotation at all, so mypy inferred Any
  for every call regardless of input shape -- zero type checking on
  one of the most heavily-used functions in the library. Added
  @overload for the four documented shapes (single ufl.Form, Sequence,
  Sequence of Sequence, None), matching the existing pattern already
  used by pack_constants/pack_coefficients/extract_function_spaces in
  the same file.
- derivative_block(): same shape-dependent-return problem, already
  spelled out explicitly in its own docstring's four cases (cases 1
  and 3 share the same static F: ufl.Form, u: Function signature, so
  they collapse into one overload). Fixing this uncovered a real,
  independent bug: NonlinearProblem.__init__ did
  `if J is None: J = derivative_block(F, u)`, and derivative_block's
  old, imprecise Union return type let that reassignment silently
  mask that the previous `J: ... | None` declared type never actually
  got narrowed past the None-check for mypy's purposes. Now fixed for
  real by the overload split.

fem/petsc.py:
- LinearProblem and NonlinearProblem made Generic[_U] (_U bound to
  Function | Sequence[Function]), with @overload on __init__ using
  the `self: LinearProblem[_Function]` / `self: LinearProblem[Sequence[_Function]]`
  self-type trick to bind _U from the shape of `a`/`L`/`F`/`u` at
  construction time. .solve() and the .u property now return _U
  directly instead of the previous flat Union, so e.g.
  `uh = LinearProblem(a, L, ...).solve(); uh.x.array[...] = ...`
  type-checks correctly instead of failing with `Item "Sequence[Function]"
  has no attribute "x"` regardless of whether a/L were given as single
  forms or block/nest sequences. Did not attempt to similarly
  parameterize the `.a`/`.L`/`.preconditioner` properties: `@property`
  cannot itself be `@overload`-ed (verified directly), and their shapes
  don't map from _U by simple identity like `.u`/`.solve()` do, so
  doing this properly would need additional correlated TypeVars for
  uncertain extra benefit -- left as the pre-existing Union.
- Fixing LinearProblem.solve() surfaced a real narrowing gap along the
  way: it assembled the preconditioner guarded by
  `if self.P_mat is not None`, but used `self.preconditioner` inside
  the block -- two separate (if always correlated) attributes, so
  mypy couldn't narrow the one actually being used. Swapped the guard
  to check `self.preconditioner` (what's actually used) plus an
  `assert self.P_mat is not None` (still true by the constructor's own
  invariant, now made explicit rather than relied upon implicitly).
  Also added the missing `| None` to LinearProblem.preconditioner's
  return type and a `_preconditioner` class-level annotation on
  NonlinearProblem, both required for these to type-check once
  derivative_block's fix stopped papering over them.

Verified with ruff check/format and mypy (-p dolfinx, demo, and test,
matching CI's exact invocation) -- both with and without petsc4py
installed in the venv, since a prior investigation this session found
that mypy silently no-ops on PETSc-touching code when petsc4py is
absent (ignore_missing_imports collapses it to Any). With petsc4py
installed, mypy demo reproduces the exact same 46 pre-existing,
unrelated PETSc-stub errors before and after this change -- confirming
zero regressions. Directly verified the fix with standalone
reveal_type scripts for both the single-form and block/nest
constructor shapes of LinearProblem and NonlinearProblem.

* fem/forms.py: reorder overloads so bare ufl.Form comes last

mypy resolves ufl.Form as Any (ufl's @ufl_type() class decorator is
unannotated), so an overload with a bare ufl.Form parameter placed
before Sequence[ufl.Form]/Sequence[Sequence[ufl.Form]]/None variants
makes those later overloads unreachable, since Any is treated as
"the same or broader" than any other type. Putting the bare-Form
overload last for form() and derivative_block() fixes the
overload-cannot-match mypy errors without changing behaviour.

* fem/forms.py: clarify docstring for None handling in form()

Explain that None can appear anywhere in a nested block-form sequence
to mark a zero block, and is passed through position-wise in the
result rather than being compiled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Tidy up

* Update docs

* Update

---------

Co-authored-by: qbisicwate <qbisicwate@gmail.com>
Co-authored-by: schnellerhase <56360279+schnellerhase@users.noreply.github.com>
Co-authored-by: Jack S. Hale <mail@jackhale.co.uk>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests that update Python code type-hints

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants