Skip to content

WIP: Cython pure Python#2135

Draft
speth wants to merge 59 commits into
Cantera:mainfrom
speth:cython-pure-python
Draft

WIP: Cython pure Python#2135
speth wants to merge 59 commits into
Cantera:mainfrom
speth:cython-pure-python

Conversation

@speth

@speth speth commented Jun 11, 2026

Copy link
Copy Markdown
Member

Changes proposed in this pull request

This is a proof of concept for converting the Cython module from the .pyx syntax to Cython's "pure Python" mode. Specific findings:

  • As anticipated in Convert Cython code to Pure Python syntax enhancements#241, Implementing this using agentic coding works just fine. Claude (Opus 4.8) figured out all of the necessary translations from pyx to the pure Python syntax.
  • Using this mode works best with each .py file being compiled into its own extension module (.so or .pyd). This means we now break up the monolithic _cantera extension module and remove our customization of the module loading process. Both the .so (or equivalent) and the .py are packaged in the wheel.
    • The one exception to this is Pyodide: here, there are linking problems with libcantera data symbols used by multiple submodules, so we fall back to the current approach of building a monolithic extension module. We may be able to revisit this as the emscripten platform evolves.
  • Having the type hints come from the .py file instead of a .pyi stub is a big usability improvement. At least in the case of Pylance, this means the popup on hover actually contains the docstring for the method/property, rather than just the type info (since the .pyi only has the type info) and ctrl-clicking takes you to the actual definition rather than the entry in the .pyi file.

I'm inclined to go ahead and implement the rest of this conversion. There aren't any other open PRs that make any significant changes to the Python module, so this seems like an okay time for something this invasive. Update: this is now in progress.

If applicable, fill in the issue number this pull request is fixing

Closes #

If applicable, provide an example illustrating new features this pull request is introducing

AI Statement (required)

  • Extensive use of generative AI. Significant portions of code or documentation were generated with AI, including
    logic and implementation decisions. All generated code and documentation were reviewed and understood by the contributor. Implemented using Claude Code (Opus 4.8).

Checklist

  • The pull request includes a clear description of this code change
  • Commit messages have short titles and reference relevant issues
  • Build passes (scons build & scons test) and unit tests address code coverage
  • Style & formatting of contributed code follows contributing guidelines
  • AI Statement is included
  • The pull request is ready for review

speth and others added 6 commits June 10, 2026 19:16
Convert jacobians.pyx to annotated jacobians.py (pure-Python Cython
syntax) and fold jacobians.pyi's public type information inline. The
companion jacobians.pxd is unchanged (augmenting-.pxd pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compile the pure-Python-syntax jacobians.py into its own shared object
shipped next to its source, instead of folding it into _cantera.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Argument annotations are real C types in pure-Python Cython mode, so the
setter must match the C++ signature setIlutFillFactor(int) rather than
the looser 'float' the old stub used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The standalone jacobians extension references pyCanteraError, a shared
symbol defined in the merged _cantera extension and used by the C++
exception-translation layer. Load _cantera with RTLD_GLOBAL (POSIX) so
such symbols resolve across extension boundaries, then restore the
previous dlopen flags. Add a smoke script for the jacobians POC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Demonstrates that with jacobians.pyi merged into the shipped jacobians.py,
mypy resolves the public types (threshold -> float, side -> Literal) for
downstream code with no separate stub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a#241)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@speth speth added the Python label Jun 11, 2026
speth and others added 4 commits June 21, 2026 11:27
Convert the scikit-build-core / CMake sdist build to the same standalone-extension
architecture the SCons build already uses, so that the Cython pure-Python-syntax
modules (enhancement Cantera#241) can ship their .py source beside the compiled extension.

- Build cantera_lib as a SHARED library instead of STATIC. On Windows, where the
  public API isn't annotated with dllexport/dllimport, WINDOWS_EXPORT_ALL_SYMBOLS
  generates the export table (mirroring the filtered .def the SCons build produces).
  Link Python::Module so pythonShim.cpp's Python C API references resolve at link
  time on Windows. Install the library beside the extensions in the package.
- Build each pure-Python Cython module (currently just jacobians) as its own
  extension linked against the shared libcantera, rather than merging it into
  _cantera. Both _cantera and the pure modules get an $ORIGIN/@loader_path RPATH so
  they find the shared library at runtime on Linux/macOS; on Windows the loader
  searches the directory containing the .pyd.
- Stop excluding jacobians.py from the wheel: with a standalone jacobians extension
  present, CPython's import system prefers the extension while the .py serves
  tracebacks, IDEs, and type checkers.

The deps fetched via FetchContent stay static (BUILD_SHARED_LIBS remains OFF); only
cantera_lib becomes shared. Verified on Windows: the wheel ships cantera_lib.dll,
jacobians.pyd, and jacobians.py; both extensions depend on cantera_lib.dll;
jacobians resolves to the extension; and the full Python test suite passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a `wheel` environment that layers the scikit-build-core / CMake toolchain
(cmake, ninja, scikit-build-core, build) onto the `core` dependencies, so the sdist
wheel route can be built and tested natively on the platforms (Windows, macOS) that
the Linux dev box can't exercise. On Windows it must be run from a shell where the
MSVC toolchain is activated (e.g. via vcvars64.bat), since cmake's Ninja generator
needs cl/link on PATH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native scikit-build-core/CMake wheel builds libcantera as a SHARED library and
each Cython pure-Python-syntax module (enhancement Cantera#241) as its own standalone
extension that links it, so the .py source can ship beside the compiled .so. That
layout does not work on Pyodide: each extension and libcantera become emscripten side
modules, and emscripten cannot resolve C++ vague-linkage data symbols (RTTI typeinfo
and vtables, which exception handling relies on) across the side-module boundary --
_cantera fails to load with "bad export type for 'typeinfo for Cantera::CanteraError'"
even when libcantera defines and exports it and is loaded globally first. It fails on
the exception base class, so it is not a single-symbol workaround, and it would only
get worse as more modules are split out.

Gate the layout on CANTERA_PYODIDE: on Pyodide, build a STATIC libcantera merged into a
single _cantera extension (cantera.<module> resolved by the CythonPackageMetaPathFinder,
as before), and exclude the raw .py sources from the wheel so they don't shadow the
merged modules. Native platforms keep the standalone-extension + shared-libcantera
layout. A developer note records the emscripten limitation so this can be revisited if
its dynamic linking of data symbols improves.

While here, link libcantera's third-party dependencies PRIVATE on the shared build and
propagate only their compile usage requirements via $<COMPILE_ONLY>, so the extensions
link just libcantera instead of each carrying a duplicate copy of the static archives
(e.g. the whole SUNDIALS stack). This mirrors the SCons build and matters as more
modules are split out. fmt stays PUBLIC because Cantera's public headers inline fmt
calls. Add `make` to the pyodide Pixi feature, needed to initialize the emscripten
cross-build environment on machines without a system make.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.48178% with 161 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.18%. Comparing base (eb7cc58) to head (5e214c2).
⚠️ Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
interfaces/cython/cantera/reactionpath.py 39.21% 31 Missing ⚠️
interfaces/cython/cantera/_onedim.py 89.55% 26 Missing ⚠️
interfaces/cython/cantera/reaction.py 92.98% 23 Missing ⚠️
interfaces/cython/cantera/reactor.py 91.60% 23 Missing ⚠️
interfaces/cython/cantera/delegator.py 86.55% 16 Missing ⚠️
interfaces/cython/cantera/jacobians.py 72.34% 13 Missing ⚠️
interfaces/cython/cantera/composite.py 99.00% 5 Missing and 1 partial ⚠️
interfaces/cython/cantera/drawnetwork.py 89.47% 3 Missing and 3 partials ⚠️
include/cantera/cython/funcWrapper.h 58.33% 2 Missing and 3 partials ⚠️
interfaces/cython/cantera/kinetics.py 97.64% 4 Missing ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2135      +/-   ##
==========================================
+ Coverage   77.81%   78.18%   +0.36%     
==========================================
  Files         452      453       +1     
  Lines       53698    54810    +1112     
  Branches     8973     8982       +9     
==========================================
+ Hits        41787    42854    +1067     
- Misses       8886     8928      +42     
- Partials     3025     3028       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

speth and others added 2 commits June 22, 2026 17:56
Convert _utils.pyx to annotated _utils.py (pure-Python Cython syntax),
keeping the companion _utils.pxd (augmenting-.pxd pattern). The .pyi stub
is retained for now; merging it inline is deferred to a separate pass.

Pure-Python Cython syntax cannot spell a C++ reference parameter, so the
two cross-module helpers that took a reference (anymap_to_py,
anyvalue_to_python) now take their argument by value in _utils.pxd. To
keep this free of extra copies, the by-value entry points are thin
wrappers that take the address of their argument and delegate to
pointer-based workers (_anymap_to_py, _anyvalue_to_py) that recurse
without copying nested children -- so deeply nested AnyMaps (e.g. a full
mechanism) are converted with the same copy behavior as the original
reference-based code. By value at the boundary is otherwise harmless:
rvalue arguments (the common parameters()/header() temporaries) are
copy-elided, and no caller relies on the previous in-place applyUnits()
side effect.

The private out-parameter helper setQuantity is likewise refactored into
_make_quantity, which returns the AnyValue by value for the caller to
assign, avoiding a reference parameter entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@speth speth changed the title WIP/POC: Cython pure Python WIP: Cython pure Python Jun 23, 2026
speth and others added 15 commits June 30, 2026 19:21
Move _utils from the merged _cantera extension into the pure_py_modules
list so it builds as its own shared object alongside its .py source. The
GIT_COMMIT define (consumed by utils_utils.h, previously applied to
_utils.pyx in the merged-extension loop) is carried into the pure-Python
build path for _utils. The now-dead _utils.pyx special case is removed
from the merged-extension loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert constants.pyx to constants.py using Cython pure-Python
(annotation) syntax and build it as a standalone extension. The module
only defines module-level physical constants sourced from the C++
extern doubles declared in the augmenting constants.pxd, so the explicit
`from .constants cimport *` is dropped (those names are already in scope
via the augmenting .pxd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert units.pyx to units.py using Cython pure-Python (annotation)
syntax and build it as a standalone extension.

The static helper `UnitStack.copy` took a `const CxxUnitStack&`
argument, which has no pure-Python Cython spelling. Since copy() always
constructs a fresh CxxUnitStack from its argument, the parameter is
changed to by-value in units.pxd; rvalue callers get guaranteed copy
elision and the single cross-module consumer (delegator) recompiles
against the updated signature with no source change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert func1.pyx to func1.py using Cython pure-Python (annotation)
syntax and build it as a standalone extension.

This is the first converted module with a C callback passed to C++ as a
function pointer (func_callback -> CxxFunc1Py). The pure-Python syntax
handles it without issue: the module-level `cdef ... except? 0.0`
callback becomes a `@cython.cfunc` with `@cython.exceptval(0.0,
check=True)`, `void*`/`void**` parameters become `cython.p_void`/
`cython.pp_void`, and referencing the cfunc by name still coerces to the
expected `callback_wrapper` function pointer. Exception propagation
through the callback is verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert reactionpath.pyx to reactionpath.py using Cython pure-Python
(annotation) syntax and build it as a standalone extension. The many
`property` blocks become `@property`/`@x.setter` pairs; typed setter
parameters keep their C types via the `cython.` prefix (`cython.double`,
`cython.int`) to preserve coercion behavior under annotation_typing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert yamlwriter.pyx to yamlwriter.py using Cython pure-Python
(annotation) syntax and build it as a standalone extension. The
write-only `property` blocks become an `@property` getter that raises
AttributeError plus an `@x.setter`. The C++ reference dereference
`deref(ptr)` is rewritten as pointer indexing `ptr[0]`, since the
`cython.operator` dereference operator is not reliably reachable from a
pure-Python `.py` body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert speciesthermo.pyx to speciesthermo.py using Cython pure-Python
(annotation) syntax and build it as a standalone extension.

This is the first converted module using typed numpy buffers. The
`cdef np.ndarray[np.double_t, ndim=1]` buffer plus `span[double](&data[0],
...)` pattern becomes: keep the numpy array as a plain object (so it is
still returned as an ndarray), bind a typed memoryview `cython.double[::1]`
over it, and build the span with `cython.address(view[0])`. Module-level
`cdef int` constants become `cython.declare(cython.int, ...)` so they stay
C ints and do not leak into the package namespace.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert mixture.pyx to mixture.py using Cython pure-Python (annotation)
syntax and build it as a standalone extension. Uses the typed-numpy-
buffer pattern for the span[double] calls (setMoles, getChemPotentials).
The `isinstance(p, ThermoPhase)` check keeps working by cimporting the
still-monolithic ThermoPhase via `from cython.cimports.cantera.thermo
import ThermoPhase`, which binds as a usable runtime type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert transport.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241.

This is the first converted module that inherits a cdef class
(_SolutionBase) defined in another extension, and the first to require
numpy's C-API: reading the inherited, ndarray-typed _selected_species
attribute compiles to PyArray_SIZE, which needs import_array() run in
this translation unit. Cython injects that call only when the module
cimports numpy, so keep `import cython.cimports.numpy as cnp` even though
the buffers themselves are now typed memoryviews.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert solutionbase.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241.

This is the first standalone module to participate in a cross-extension
cyclic cimport: it defines _SolutionBase (cimported by thermo, kinetics,
transport, and reaction) while itself cimporting those derived classes
for isinstance checks. The cycle resolves at import time without special
handling. As with transport, the inherited ndarray-typed
_selected_species attribute requires numpy's C-API, so the numpy cimport
is retained to trigger import_array().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert kinetics.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241.

The free function get_from_sparse, which is cimported by the
already-converted jacobians module, took a CxxSparseMatrix reference
argument. Since pure-Python annotation syntax has no spelling for a C++
reference parameter, its declaration in kinetics.pxd is changed to pass
the matrix by value; all callers pass rvalues returned by the C++ core,
so this is copy-elided. As with the other phase modules, the numpy
cimport is retained so that import_array() runs for the C-API access via
the inherited _selected_species attribute.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert reaction.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241. reaction.pxd is
unchanged. The numpy cimport is retained so import_array() runs for the
typed-ndarray buffers and the ndarray rate arguments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert thermo.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241. thermo.pxd is
unchanged. The numpy cimport is retained so import_array() runs for the
typed-ndarray state-vector buffers and the inherited _selected_species
access.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert reactor.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241. reactor.pxd is
unchanged. The numpy cimport is retained so import_array() runs for the
typed-ndarray state/sensitivity buffers.

The ExtensibleReactor classes wrap a C++ span as a non-owning memoryview
(the .pyx `<double[:n]> ptr` sized pointer cast); pure-Python annotation
syntax has no equivalent, so this uses the cython.view.array idiom with
allocate_buffer=False and a statically typed array whose data pointer is
assigned directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert _onedim.pyx to pure-Python (annotation) syntax built as a
standalone extension, following enhancement Cantera#241. _onedim.pxd is
unchanged. The numpy cimport is retained so import_array() runs for the
typed-ndarray grid/profile buffers, and the Domain1D.grid getter wraps
the C++ span via the cython.view.array idiom.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
speth and others added 22 commits June 30, 2026 19:21
Second module of the .pyi stub-merge pass (enhancement Cantera#241): fold the public
type annotations from constants.pyi inline into constants.py and delete the
stub, so the annotated .py is the single source of the published type surface.

Each module-level constant gains an inline ``: float`` annotation matching the
former stub. In Cython pure-Python mode this annotation does not turn the global
into a C-only variable: the values remain ordinary Python module attributes
(verified that cantera.avogadro and cantera.constants.avogadro still resolve and
are of type float). Because the assignments reference the C++ ``Cxx*`` names
injected by constants.pxd (undefined to a plain Python parse), cantera.constants
is added to mypy's ignore_errors list, matching cantera.func1 and the other
pure-Python modules. No stubtest allowlist entries are needed (constants exposes
no C-level helpers).

Verified: mypy clean, stubtest 0 findings, pyright --verifytypes unchanged (all
constants symbols known), namespace-cleanliness and constants tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Third module of the .pyi stub-merge pass (enhancement Cantera#241), and the first to
establish two patterns the remaining modules need.

(1) `new`→`make_shared`: `__cinit__` used the C++ `new` operator
(`self._writer.reset(new CxxYamlWriter())`), which is invalid Python syntax and so
blocks dropping the stub (mypy/stubtest fall back to a strict Python parse of the
.py). Replaced with `self._writer = make_shared[CxxYamlWriter]()`, which is valid
Python *and* Cython. No C++ factory is needed here since the return type is not
polymorphic (per Ray); `make_shared` is cimported from `libcpp.memory`. As a
cimported name it is absent at runtime, so the stubtest allowlist's general
cimported-helper pattern gains `make_shared`.

(2) Sibling cdef classes in public annotations (`_SolutionBase`, `UnitSystem`):
these must be resolvable by mypy/pyright (which cannot see `cython.cimports` or the
.pxd), so they are imported as ordinary Python imports; the companion .pxd cimports
the same names so Cython still treats them as C-level extension types (needed for
e.g. `soln.base`). `UnitSystem` is aliased to `_UnitSystem` so it is not re-exported
by `from .yamlwriter import *` (test_namespace_cleanliness); `_SolutionBase` already
starts with an underscore. The `output_units` setter accepts a UnitSystem *or* a
units map, so its annotation uses the Python-imported `_UnitSystem` (param stays a
plain object — no C typing) which keeps the body's `isinstance`/conversion branch
working for the dict path (verified at runtime).

cantera.yamlwriter is added to mypy's ignore_errors list and the
`@cython.cfunc @staticmethod` helper `_get_unitsystem` (absent at runtime) to the
stubtest allowlist.

Verified: mypy clean, stubtest 0 findings, pyright --verifytypes unchanged (96.7%;
cantera.YamlWriter fully known), runtime smoke covers both output_units paths and
the make_shared __cinit__, and 58 yaml/output_units/namespace tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fourth module of the .pyi stub-merge pass (enhancement Cantera#241). reactionpath already
used a factory (`newReactionPathDiagram`) so it needed no `new`/make_shared change;
it has no internal `@cython.cfunc` helpers, so no stubtest allowlist entries.

Two points of note:

- Published constructor needs a typed `__init__`: the work is done in `__cinit__`,
  which mypy/pyright do not recognize as the constructor (a consumer doing
  `ReactionPathDiagram(gas, "H")` was reported as "Expected 0 positional arguments"
  after dropping the stub). Added a typed `__init__` carrying the published signature
  (the C++ diagram is still built in `__cinit__`), mirroring func1's structure. The
  former stub's parameter name `contents` did not match the runtime `__cinit__`
  parameter `phase`; the merged signature uses the runtime-truthful `phase`.

- `_SolutionBase` (constructor annotation) is imported as an ordinary Python import
  for the checkers and is available to Cython as a C-level type via the .pxd's
  `from .kinetics cimport *` chain; it already starts with an underscore so it is not
  re-exported by `from .reactionpath import *`.

Basic-type annotations use the Python spellings (`bool`, `float`, `str`, `int`) and
the flow_type Literal is live, all matching the former stub. cantera.reactionpath is
added to mypy's ignore_errors list.

Verified: mypy clean, stubtest 0 findings, and a consumer check confirms the full
surface is preserved -- `ReactionPathDiagram(gas, "H")` type-checks, a wrong first
arg is rejected (`phase` must be `_SolutionBase`), `d.flow_type` is
`Literal['NetFlow', 'OneWayFlow']`, `d.build` is `(verbose: bool = False) -> None`.
pyright completeness is 96.6% (was 96.7): no new unknown -- the 79 unknowns remain
entirely in cantera.with_units; the rounding shift is purely the known-symbol
denominator shrinking because the stub listed members at module level while the .py
nests them under the class (a documented counting artifact, not a surface loss).
6 reactionpath/namespace tests pass; runtime smoke builds a diagram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fifth module of the .pyi stub-merge pass (enhancement Cantera#241).

- Constructor: SpeciesThermo builds itself in __cinit__, so a typed __init__ is
  added to publish the constructor signature (T_low/T_high/P_ref/coeffs/init), as
  with reactionpath; the real work stays in __cinit__ and the parameters remain
  documented in the class docstring.
- The _SpeciesThermoInput TypedDict (return type of input_data) is folded inline,
  and the base class gains the `derived_type: ClassVar[int]` annotation; the
  subclasses already assign it at runtime.
- Published annotations use Python spellings (float, int, bool, dict[str, Any]) and
  the numpy aliases _Array/_ArrayLike from ._types.
- Allowlist additions for names compiled away at runtime: the module-level C ints
  `SPECIES_THERMO_*` (cython.declare), the `@cython.cfunc` helpers
  SpeciesThermo._assign and wrapSpeciesThermo (the latter is cimported by thermo.py
  and unaffected), plus the cimported _utils helpers anymap_to_py/py_to_anymap added
  to the general cimported-helper allowlist pattern.

cantera.speciesthermo is added to mypy's ignore_errors list.

Verified: mypy clean, stubtest 0 findings, pyright --verifytypes has no new unknown
(79 unknowns all remain in cantera.with_units; score 96.6% reflects the documented
member-count denominator artifact, not a surface loss). A consumer check confirms
the surface: `NasaPoly2(...)` type-checks, a wrong T_low is rejected, input_data is
`_SpeciesThermoInput`, cp is `(T: float) -> float`. 22 speciesthermo/namespace tests
pass and a runtime smoke covers construction, properties, input_data, and a
coeffs roundtrip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Sixth module of the .pyi stub-merge pass (enhancement Cantera#241), and the first to carry
`@overload` method signatures.

- Overloads: UnitSystem.convert_to / convert_activation_energy_to /
  convert_rate_coeff_to each get their three `@overload` stubs prepended to the
  existing implementation. Cython compiles the stubs (the final undecorated `def` is
  the runtime method) and stubtest accepts them; a consumer confirms the published
  return types narrow correctly (`convert_to("3 cm", "m")` -> float,
  `convert_to([...], "m")` -> list[float], `convert_rate_coeff_to(..., UnitStack())`
  -> float). The implementation bodies and their existing arg annotations are left
  unchanged so runtime behavior is identical (this faithfully preserves the prior
  surface, including the stub's `dest: str | Units` on convert_activation_energy_to
  even though the body only handles a string -- a pre-existing discrepancy, not
  something to "fix" in a mechanical merge).
- `new CxxUnitSystem()` -> `make_shared[CxxUnitSystem]()`.
- Typed `__init__` added for Units and UnitSystem (their constructors take args);
  UnitStack needs none (no-arg constructor, default is correct).
- The `_UnitDict` TypedDict is folded inline; `_Array` comes from ._types.
- Allowlist additions for compiled-away names: the `@cython.cfunc` staticmethods
  Units.copy / UnitStack.copy and the cfunc UnitSystem._set_unitSystem, plus the
  cimported _utils helper python_to_anyvalue added to the general allowlist pattern.

cantera.units is added to mypy's ignore_errors list.

Verified: mypy clean, stubtest 0 findings, pyright --verifytypes no new unknown (79
unknowns all remain in cantera.with_units). 610 units/convert/namespace tests pass
and a runtime smoke covers the scalar/list/array convert_to paths, Units, and
UnitStack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Seventh module of the .pyi stub-merge pass (enhancement Cantera#241).

- Ownership: the raw `new CxxMultiPhase()` (with a manual `del` in __dealloc__) is
  replaced by the codebase-standard pattern -- a `shared_ptr[CxxMultiPhase] _mix`
  owner constructed with `make_shared`, plus the existing raw `mix` pointer as a
  non-owning accessor (`self.mix = self._mix.get()`). __dealloc__ is removed; the
  shared_ptr member is destroyed automatically, so memory management is equivalent.
  Verified at runtime through construction, equilibration, and `del` with no crash.
- phase_moles gets its two `@overload` stubs (p -> float; no-arg -> list[float]).
- ThermoPhase (used throughout the annotations and in the phase_index isinstance
  check) switches from a `cython.cimports` cimport to an ordinary Python import under
  the `_ThermoPhase` alias: mixture needs no C-level access to it, so the single
  Python import both satisfies the checkers and serves the isinstance check, and
  avoids a phantom `mixture.ThermoPhase` that the cimport made stubtest flag.
- Annotations use Python spellings and the ._types aliases (_Array/_ArrayLike/
  _EquilibriumSolver/_LogLevel/_PropertyPair). element_index keeps `-> cython.int`
  to match its cpdef declaration; pyright resolves it to `int`. No new stubtest
  allowlist entries are needed (no @cython.cfunc helpers).

cantera.mixture is added to mypy's ignore_errors list.

Verified: mypy clean, stubtest 0 findings, pyright --verifytypes no new unknown
(79 unknowns all in cantera.with_units). A consumer confirms the surface
(phase_moles overloads, phase -> ThermoPhase, element_index -> int, equilibrate
rejects a non-PropertyPair literal). 197 mixture/multiphase/namespace tests pass and
a runtime smoke covers the full lifecycle plus deletion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fold the published annotations from transport.pyi inline into transport.py and
delete the stub, so the .py is the single source of the published type surface.

Transport is the first merged Cython class that is a *base class* of pure-Python
classes (Solution, DustyGas, _WaterWithTransport), which required more than a
plain annotation merge to keep the type checkers correct:

- Convert C++ `new CxxGasTransportData(...)` to `make_shared[...]`, so the .py
  parses as valid Python (mypy/stubtest parse the .py once the stub is gone).
- Import `_SolutionBase` via a plain `from .solutionbase import ...` instead of
  `cython.cimports`, so mypy/pyright resolve Transport's base to the solutionbase
  stub. The C-level base still comes from transport.pxd, unchanged.
- Give Transport.__init__ / DustyGasTransport.__init__ the explicit published
  constructor signature (forwarding to super()), so the precise _SolutionBase
  constructor is not shadowed in the MRO by an untyped *args/**kwargs forwarder.
  Runtime forwarding and the standalone-instantiation guard are unchanged.
- Add cantera.transport to the mypy opaque list and add the @cython.cfunc helpers
  plus three compile-time-only / stubtest-quirk names to the stubtest allowlist.

mypy and stubtest pass; the full runtime suite passes; consumer-side typing
(reveal_type) of Transport/Solution is correct. pyright --verifytypes (a
non-gating coverage report) drops because it cannot resolve a @cython.cclass
base class for completeness purposes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fold the published annotations from kinetics.pyi inline into kinetics.py and
delete the stub. Like transport, Kinetics is a base class of pure-Python classes
(Solution, Interface, DustyGas), so this follows the same pattern:

- Import _SolutionBase via a plain `from .solutionbase import ...` (the C-level
  base still comes from kinetics.pxd) so the checkers resolve the base class.
- Give Kinetics.__init__ and InterfaceKinetics.__init__ the explicit published
  constructor signature, forwarding to super(); the standalone-instantiation
  guard and InterfaceKinetics phase-index setup are unchanged.
- Annotation-only sibling types (Reaction, CustomRate, ThermoPhase, UnitSystem,
  ...) are imported under TYPE_CHECKING; the two methods that C-access a Reaction
  param (add_reaction, modify_reaction) take the alias and cast in the body.
- reaction(): route the index through a `cython.int` local so a negative index
  wraps to size_t and reaches the C++ range check (matching prior behavior) now
  that the published param type is `int` rather than `cython.int`.
- Add cantera.kinetics to the mypy opaque list and the @cython.cfunc helpers plus
  compile-time-only cimports to the stubtest allowlist.

mypy and stubtest pass; full runtime suite passes (kinetics/reaction/composite/
purefluid/onedim/reactor). kinetics's own published surface is 100% known to
pyright; the non-gating --verifytypes total dips only because Interface joins the
already-unknown Solution/DustyGas set (a @cython.cclass base it inherits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fold the published type annotations from solutionbase.pyi inline into
solutionbase.py and delete the stub, so the .py is the single source of the
published type surface for `_SolutionBase` and `SolutionArrayBase`.

`_SolutionBase` is the root base class of the `Solution` hierarchy
(ThermoPhase/Kinetics/Transport and Solution/Interface/DustyGas), so this
follows the base-class merge recipe: a typed `__init__` carrying the published
constructor signature (the runtime work stays in `__cinit__`/`_cinit`),
sibling cdef-class annotations under TYPE_CHECKING, and the C++ `new
CxxPythonHandle(...)` rewritten as make_shared + static_pointer_cast.

Component/`extra` name marshalling methods receive `numpy.str_` and the private
`_cxx_save`/`_cxx_restore` interfaces receive `Path`/`None`; a bare `str`
annotation makes Cython's annotation_typing reject these (it is stricter than
PEP 484), so those params use a `str` alias / accurate unions to keep the
published types while preserving the pre-merge runtime behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
The .pyi-merge `new CxxFoo(...)` -> `make_shared[CxxFoo](...)` conversion
silently dropped C++ exception translation: libcpp.memory.make_shared is
declared `except +`, so a CanteraError thrown by a C++ constructor surfaced
as a generic RuntimeError instead of cantera.CanteraError (e.g. constructing
a Reaction with a negative pre-exponential factor). The original
`new CxxFoo(...)` expressions translated because the constructors are declared
`except +translate_exception` in the .pxd files.

Declare a translate-aware `make_shared` in ctcxx.pxd that shadows the libcpp
one, and have the converted modules pick it up via `from .ctcxx cimport *`
instead of importing libcpp.memory.make_shared.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
composite is plain Python (not Cython), so it is now type-checked by mypy
strict rather than added to the ignore_errors list. SolutionArray's
single-property accessors are read-only properties installed at runtime by
the setattr loops; the inline bare annotations publish the correct type but
not read-only-ness (mypy strict rejects every compact read-only spelling),
so the read-only/read-write mismatch is allowlisted for stubtest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
_onedim is a Cython pure-Python module whose classes (Domain1D, Sim1D, and
the flow/boundary hierarchy) are base classes for the plain-Python onedim
module. Following the base-class merge pattern, the base _SolutionBase is
imported via a plain Python import (checker-visible) while the augmenting
.pxd keeps the cimport for the C type; sibling cdef-class names used only for
C-attr access in bodies stay bare (resolved via the .pxd cimport), which is
safe because _onedim is added to the mypy ignore_errors list.

Parametrized-generic return/argument annotations (dict[str, str] for
Sim1D.restore, tuple[float, float] for bounds) are routed through
TypeAliases to avoid Cython 3's annotation_typing coercion, which would
reject an AnyMap (dict subclass) return value or a list argument at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
onedim is plain Python (not Cython), so it is now type-checked by mypy
strict rather than added to the ignore_errors list. All 69 methods across
the 7 flame classes are annotated; `from __future__ import annotations`
keeps annotations lazy. FlameBase's ~95 gas/kinetics property pass-throughs
are read-only `property` objects installed at import time by the setattr
loops, so the inline bare annotations publish the correct type but not
read-only-ness; that mismatch is allowlisted for stubtest, as are the two
function-local exception classes that stubtest's static parse misreports.

A handful of precise `# type: ignore[code]` mark genuine discrepancies:
SolutionArray.TP's setter broadcasts scalars while its published type
matches the getter, and two pre-existing stub bugs (to_pandas declared
-> Array but returns a DataFrame; Sim1D.solve_adjoint declared -> None but
returns an ndarray) are carried forward faithfully rather than widening the
published surface.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
reactor is a Cython pure-Python module, so it is added to the mypy
ignore_errors list; its published surface is covered by stubtest and
pyright. All 26 reactor/surface/connector/network classes are annotated
inline. Sibling types used only in annotations (Solution, _Func1Like,
_DerivativeSettings, graphviz.Digraph) are imported under TYPE_CHECKING and
string-annotated; SystemJacobian and the cdef-class types whose bodies need
C-attribute access stay resolved via the .pxd cimport (safe under
ignore_errors). ReactorNet.solver_stats returns an AnyMap (a dict subclass)
from anymap_to_py, so its dict[str, int] return is routed through a
TypeAlias to avoid Cython's annotation_typing coercion.

Two stub bugs are fixed against .pxd/runtime truth: surface_production_rates
is declared on ExtensibleReactorSurface (not the ReactorSurface base, where
the stub mistakenly placed it), and Reactor.group_name is annotated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Fold the published ``extension`` signature inline and delete the stub. The
``ExtensibleRate``/``ExtensibleRateData`` references are imported under
``TYPE_CHECKING`` aliases (the runtime cimports are kept for the ``issubclass``
checks) and string-annotated to avoid an import cycle, since ``delegator`` is
initialized before ``reaction``. Add ``cantera.delegator`` to the mypy opaque
list and allowlist its compile-time-only cimports for stubtest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
Delete the package stub; the inline annotations now carried by every submodule
plus the ``_cantera`` aggregator source let the type checkers resolve the full
``cantera`` surface through the real import chain. Re-export the version dunders
with explicit ``as`` aliases (required for mypy's no-implicit-reexport) and
mark the ``del`` of the star-imported ``np``/``os`` as the names the checkers
cannot track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
``composite`` imported ``ThermoPhase``/``Kinetics``/``Transport``/``PureFluid``
and friends from the ``_cantera`` aggregator. pyright does not reliably
re-export a name through ``_cantera``'s ``from .X import *`` chain, so those
names resolved to ``Unknown`` and ``Solution``'s inherited ``ThermoPhase`` and
``Kinetics`` members were invisible to the type checkers / IDEs. Import them
directly from ``.kinetics`` / ``.thermo`` / ``.transport`` / ``.solutionbase``
instead (as the former ``composite.pyi`` stub did, and now possible at runtime
since those modules are standalone extensions). The classes are still needed at
runtime as base classes, and ``composite`` is imported after every submodule,
so there is no import cycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPNco77PDEgwTxvtaaCHt6
@speth speth force-pushed the cython-pure-python branch from 1bac505 to 5e214c2 Compare June 30, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant