Skip to content

Generate and ship committed PEP 561 type stubs for the Python extension - #912

Merged
IvanaGyro merged 5 commits into
masterfrom
claude/pybind11-stubgen-stubs-ebny1c
Jul 8, 2026
Merged

Generate and ship committed PEP 561 type stubs for the Python extension#912
IvanaGyro merged 5 commits into
masterfrom
claude/pybind11-stubgen-stubs-ebny1c

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jun 17, 2026

Copy link
Copy Markdown
Member

Closes #585.

What this does

Ships committed type information with the cytnx wheel so IDEs and static type checkers can introspect the compiled pybind11 extension (cytnx.cytnx) — autocompletion, hover docs, mypy/pyright/stubtest.

  • Committed stubs. tools/generate_stubs.py runs pybind11-stubgen against a built extension and writes sanitized cytnx/cytnx/*.pyi files, which are committed and shipped — unchanged, platform-independent — in every wheel and conda package via tool.scikit-build.wheel.packages.
  • Not generated during the build. Build-time generation has to import the freshly built, architecture-specific extension, which is impossible when cross-compiling and brittle in build sandboxes (e.g. the conda MKL build, whose isolated prefix doesn't expose libatomic), and forces pybind11-stubgen + import-time deps into every build environment. A .pyi describes only the public API, so generating it once and committing it is sufficient for all platforms; the committed cytnx/cytnx/ directory has no __init__.py, so it doesn't shadow the extension at import time. Keeping the committed stubs honest with a CI mypy.stubtest job is planned (not yet wired into CI by this PR).
  • PEP 561. Adds the cytnx/py.typed marker.
  • Stub sanitizing. Raw C++ types that stubgen renders as a bare ... are rewritten to typing.Any so the committed stubs are valid for every consumer.

How other scientific packages ship stubs

Survey of how comparable packages with a compiled (C / Cython / C++) extension provide type information, along the axes this PR has to decide: do they ship a py.typed marker in their own wheel, are the extension stubs hand-written or tool-generated, are they committed or regenerated, do they have to import the built binary at build time to produce them, and how does CI keep them honest.

Project py.typed How compiled parts are typed Committed or generated Imports binary at build to make stubs Sync check in CI
NumPy yes hand-written .pyi in-tree committed no stubtest (imports only to verify)
SciPy via scipy-stubs hand-written .pyi committed (separate pkg) no stubtest
matplotlib yes hand-written .pyi for C++ exts committed no stubtest + regenerate-and-diff
Pillow yes hand-written .pyi (_imaging.pyi, …) committed no mypy only
PyArrow yes hand-written .pyi under pyarrow-stubs/, copied into the wheel via CMake install(... PATTERN "*.pyi") committed no (only optional docstring injection) configured, not yet gating
pandas via pandas-stubs hand-curated .pyi committed (separate pkg) no stubtest
pydantic-core yes hand-written .pyi committed no stubtest
PyTorch yes generated from YAML + .pyi.in templates gitignored, regenerated every build no — static codegen, never imports the binary lint check
scikit-learn / h5py / shapely / lxml no first-party types
cytnx (this PR) yes generated .pyi via pybind11-stubgen, sanitized committed no — generated offline once, not during the build none yet; mypy.stubtest is planned (#585)

Takeaways for cytnx's choices:

  • In-wheel py.typed, not a separate *-stubs package. Like NumPy, matplotlib, Pillow, PyArrow, and pydantic-core — and unlike SciPy/pandas, whose public-API stubs live in separately-installed scipy-stubs/pandas-stubs — a plain pip install cytnx is fully typed with nothing extra to install.
  • Generated, but committed. Every other generated case here is PyTorch, which gitignores its .pyi and regenerates them on every build via static codegen from YAML/templates (no binary import). cytnx can't mirror that: pybind11-stubgen reads the built extension by importing it, which is exactly what cross-compiled / sandboxed builds can't do. So cytnx generates once and commits the result, landing in the same place as the hand-written-and-committed crowd (NumPy, Pillow, …) while avoiding hand-maintenance.
  • No binary import at build time. Column 5 is no for every project, cytnx included — generation is a deliberate offline step, never part of the wheel build.
  • CI sync check. This PR does not add one. NumPy/SciPy/pandas/pydantic-core all gate on mypy.stubtest; wiring an equivalent job into this repo's CI is left as follow-up work under Generate stub files #585.

Sources: NumPy multiarray.pyi + .spin stubtest task; scipy-stubs; matplotlib stubtest CI; Pillow _imaging.pyi; PyArrow developer docs; pandas-stubs; pydantic-core .pyi; PyTorch torch/_C/__init__.pyi.in + .gitignore.

Binding fixes required for valid stubs

Pre-existing bindings produced signatures that are invalid or lossy Python and broke the generated stubs:

  • Network.construct bound outrk with no default although the C++ API defaults it to 0 (a non-default-after-default signature). Now bound with its = 0 default.
  • UniTensor.put_block / put_block_ named their leading Tensor argument in, a Python reserved word. Renamed to Tin (the convention used elsewhere in the UniTensor binding). Positional calls are unchanged; the only form that could pass it by name — put_block(**{'in': block}) — keeps working via a trailing (*args, **kwargs) overload that emits a FutureWarning steering callers to Tin, while conflicting/invalid argument lists raise TypeError.
  • Top-level Tensor factories (zeros/ones/arange/eye/identity/linspace/_from_numpy) typed as -> typing.Any because generator_binding ran before tensor_binding registered the Tensor class, so pybind11 could not resolve their -> Tensor return. Reordering generator_binding after tensor_binding makes them type as -> Tensor directly (binding-layer fix, no stub post-processing).

Notes

  • Rebased onto master after Remove untested tn_algo Python bindings for non-functional C++ code #922 removed the tn_algo Python bindings, so the committed stubs no longer include tn_algo.pyi.
  • pybind11 is pinned to ==3.0.4 in [build-system].requires. pybind11 controls the type annotations baked into the extension that tools/generate_stubs.py reads, so a floating version silently rewrites the committed stubs (3.0.4, for example, annotates integer arguments as typing.SupportsInt | typing.SupportsIndex); pinning keeps them reproducible, and the pin is bumped deliberately alongside a regeneration. The pin lands before the stub-generation commit, so the stubs are produced against it directly with no follow-up regeneration commit.
  • Stubs are regenerated with the lowest supported interpreter (Python 3.10) against the pinned pybind11.
  • Commits: Network.construct default, put_block rename, pin pybind11 3.0.4, committed stubs + tooling, order generator_binding after tensor_binding.
  • The committed stubs are regenerated against the built extension, so e.g. __version__ tracks version.cmake.

Verification

  • pytest pytests/put_block_test.py: 12 passed (positional, Tin=, deprecated **{'in': ...} with FutureWarning, and conflicting-argument rejection).
  • Generated stubs parse as valid Python and resolve overloaded signatures (e.g. cytnx.zerosTensor, cytnx.linalg.Svd, UniTensor.put_block).

Follow-ups (sub-issues of #585)

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces PEP 561 type stub generation using pybind11-stubgen during the CMake build process, updates build-system requirements in pyproject.toml, and renames the reserved keyword argument 'in' to 'Tin' in UniTensor's put_block and put_block_ bindings while maintaining backward compatibility with deprecation warnings. Feedback on these changes suggests adding a CMake option to disable stub generation to support cross-compilation and prevent installation failures. Additionally, the reviewer recommends explicitly handling potential conflicts in put_block and put_block_ when both 'in' and 'Tin' keyword arguments are provided simultaneously.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread CMakeLists.txt Outdated
Comment thread CMakeLists.txt Outdated
Comment thread pybind/unitensor_py.cpp
Comment thread pybind/unitensor_py.cpp
@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.25490% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.04%. Comparing base (21cf05a) to head (5c90a2e).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
pybind/unitensor_py.cpp 40.42% 0 Missing and 28 partials ⚠️
pybind/network_py.cpp 0.00% 0 Missing and 3 partials ⚠️
pybind/cytnx.cpp 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #912      +/-   ##
==========================================
+ Coverage   29.87%   30.04%   +0.16%     
==========================================
  Files         240      240              
  Lines       35425    35460      +35     
  Branches    14729    14752      +23     
==========================================
+ Hits        10584    10653      +69     
+ Misses      17593    17520      -73     
- Partials     7248     7287      +39     
Flag Coverage Δ
cpp 29.66% <37.25%> (+0.16%) ⬆️
python 52.45% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 31.29% <ø> (+0.11%) ⬆️
Python bindings 17.73% <37.25%> (+0.66%) ⬆️
Python package 52.45% <ø> (ø)
Files with missing lines Coverage Δ
pybind/cytnx.cpp 18.75% <0.00%> (ø)
pybind/network_py.cpp 20.54% <0.00%> (-0.29%) ⬇️
pybind/unitensor_py.cpp 11.04% <40.42%> (+2.42%) ⬆️

... and 2 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 21cf05a...5c90a2e. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@IvanaGyro
IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from c7473d7 to 6700547 Compare June 20, 2026 01:20
@IvanaGyro IvanaGyro changed the title Generate PEP 561 type stubs for the Python extension at build time Generate and ship committed PEP 561 type stubs for the Python extension Jun 20, 2026
@IvanaGyro
IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch 2 times, most recently from bffc010 to c1e1f45 Compare June 20, 2026 06:12
@IvanaGyro
IvanaGyro marked this pull request as ready for review June 20, 2026 07:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1e1f453a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cytnx/cytnx/linalg.pyi Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 725a8ddd4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cytnx/cytnx/tn_algo.pyi Outdated
IvanaGyro and others added 2 commits June 21, 2026 08:37
The C++ Network::construct declares outrk with a default value (= 0, see
include/Network.hpp), but the pybind11 binding bound it with a bare
py::arg("outrk") and no default. This had two consequences:

- Python callers were forced to pass outrk even though the C++ API treats
  it as optional.
- A required parameter following the defaulted outlabel produces a function
  signature with a non-default argument after a default one, which is
  invalid Python. Tools that materialise the signature (e.g. type-stub
  generators) emit a syntactically invalid result for this overload.

Bind outrk with its C++ default (= (cytnx_int64)0) so the Python signature
matches the C++ API and is a well-formed Python signature.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egacy keyword

put_block and put_block_ bound their leading Tensor argument as py::arg("in").
'in' is a Python reserved word, so it could never be passed as put_block(in=...)
(a SyntaxError) and any materialised signature naming it 'in' is invalid Python,
which breaks type-stub generation.

Rename the argument to 'Tin' -- the name already used for Tensor inputs elsewhere
in the UniTensor binding -- and preserve the only previously usable legacy form,
put_block(**{'in': block}): a trailing (py::args, py::kwargs) overload detects an
'in' keyword, emits a FutureWarning, rewrites it to 'Tin', and forwards to the
typed overloads. Supplying both 'in' and 'Tin' raises TypeError, and argument
lists containing neither still raise TypeError so invalid calls are rejected. The
existing 'force'-deprecation warning messages are updated from 'in' to 'Tin'.

Add put_block_test.py covering the 'Tin' keyword, the deprecated 'in' keyword
(and its warning), the conflicting-argument rejection, and positional calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from 725a8dd to 37e3210 Compare June 21, 2026 17:22

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 37e32102a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/generate_stubs.py Outdated
Comment thread cytnx/cytnx/__init__.pyi
Comment thread cytnx/cytnx/linalg.pyi
IvanaGyro and others added 2 commits June 21, 2026 19:32
tools/generate_stubs.py reads the type annotations pybind11 bakes into
the extension to produce the committed cytnx/cytnx/*.pyi stubs. Those
annotations change between pybind11 releases (for example, 3.0.4 began
emitting `typing.SupportsIndex` alongside `typing.SupportsInt` for
integer arguments), so a floating `pybind11 >=3.0` requirement silently
rewrites the committed stubs whenever the resolved version moves.

Pin the build requirement to `pybind11 ==3.0.4` so the stubs generated
by the following commit stay byte-for-byte reproducible. Bump this pin
deliberately, together with a stub regeneration, when moving to a newer
pybind11.
Ship committed type information with the cytnx wheel so IDEs and static
type checkers can introspect the compiled pybind11 extension
(cytnx.cytnx): autocompletion, hover docs, mypy/pyright/stubtest.

- tools/generate_stubs.py runs pybind11-stubgen against a built
  extension and writes sanitized cytnx/cytnx/*.pyi files. They are
  committed and shipped unchanged in every wheel and conda package via
  the existing tool.scikit-build.wheel.packages = ["cytnx"] (the stubs
  live inside the package). The committed cytnx/cytnx/ directory has no
  __init__.py, so it does not shadow the extension at import time.
- Generation is a deliberate offline step, never part of the build:
  pybind11-stubgen must import the freshly built, architecture-specific
  extension, which is impossible when cross-compiling and brittle in
  build sandboxes (e.g. the conda MKL build, whose prefix does not
  expose libatomic). A .pyi describes only the public API, so generating
  once and committing is sufficient for every platform.
- Add the cytnx/py.typed marker (PEP 561), and the pybind11-stubgen
  generation dependency to the dev extra.
- Raw C++ types that stubgen renders as a bare `...` (e.g. cytnx::Tensor
  in a docstring) are rewritten to typing.Any so the committed stubs are
  valid for every consumer.

The stubs are generated with the lowest supported interpreter
(Python 3.10) against the pinned pybind11 (==3.0.4, previous commit), so
regeneration reproduces the committed files byte-for-byte.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the claude/pybind11-stubgen-stubs-ebny1c branch from 37e3210 to 9e7e777 Compare June 21, 2026 19:33

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e7e777dd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cytnx/cytnx/__init__.pyi Outdated
Comment thread cytnx/cytnx/__init__.pyi
… Tensor

The module-level Tensor factories (zeros, ones, arange, eye, identity,
linspace, _from_numpy) are bound in generator_binding, which ran before
tensor_binding registered the Tensor py::class_. With Tensor not yet
registered when the factories were bound, pybind11 could not resolve
their `-> Tensor` return annotation and emitted a raw C++ type, which
pybind11-stubgen rendered as a bare `...` and tools/generate_stubs.py
sanitized to `typing.Any`. Consumers of the shipped stubs therefore lost
the return type: `cytnx.zeros([2, 2])` typed as Any, discarding all
downstream Tensor method checking.

Move generator_binding after tensor_binding so the Tensor class is
registered first, and regenerate cytnx/cytnx/__init__.pyi: the factories
now type as `-> Tensor`. Fixing it at the binding site keeps
tools/generate_stubs.py free of a per-function return-type override.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c90a2e7c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cytnx/cytnx/__init__.pyi
@IvanaGyro

Copy link
Copy Markdown
Member Author

@pcchen Can you help to review this?

@IvanaGyro

Copy link
Copy Markdown
Member Author

@pcchen @yingjerkao Is there any concern about this PR?

@yingjerkao yingjerkao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I will approve this, but my only concern is what the mechanism to sync with the C++ API changes ?

@IvanaGyro

IvanaGyro commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

I will approve this, but my only concern is what the mechanism to sync with the C++ API changes ?

@yingjerkao Developers need to manually generate the stubs if the Python APIs change with C++ API. Per what was mentioned in this PR description, generating at build time is almost impossible. And generating before CI will lost flexibility to fine tune the stubs.

We should add a section about the generating process in Contributing.md. I didn't add it because there was a PR creating the same file at the time. I will open another PR for adding the section.

@IvanaGyro
IvanaGyro merged commit b4c1584 into master Jul 8, 2026
19 checks passed
@IvanaGyro
IvanaGyro deleted the claude/pybind11-stubgen-stubs-ebny1c branch July 8, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generate stub files

2 participants