Skip to content

Fix remaining overload-cannot-match errors with the #915 keep-set strategy#1053

Merged
yingjerkao merged 27 commits into
masterfrom
fix/overload-cannot-match-remaining
Jul 17, 2026
Merged

Fix remaining overload-cannot-match errors with the #915 keep-set strategy#1053
yingjerkao merged 27 commits into
masterfrom
fix/overload-cannot-match-remaining

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

mypy.stubtest reported 768 overload-cannot-match errors across the pybind11 bindings (Tensor, UniTensor, Storage, Scalar, LinOp, Symmetry) after PR #915 fixed the same class of error for ExpH/ExpM. The root cause was the same in every case: a method registered a raw .def(...) overload per C++ dtype (11+ per operator) alongside — or ahead of — an already-correct numpy_scalar-typed keep-set, so pybind11-stubgen emitted duplicate or identically-rendered @typing.overload signatures that mypy flags as unreachable. A few of these coexisting raw registrations were also ordered before their numpy_scalar counterpart, which is a live reachability bug (documented as the "__index__ no-convert trap" / "__float__-fallback trap" in pybind/pyint_dispatch.hpp), not just a stub-quality issue.

Fix

Applied #915's keep-set consolidation (one overload per Python-distinguishable dtype, numpy_scalar block first, then py::int_/Scalar/double/complex128) across the remaining binding surface:

  • Tensor/UniTensor: collapsed fill/append/set_elem/arithmetic-operator overload sets; reordered Scalar ahead of double/complex128 where it had been registered after (verified this does not change pass-1 overload resolution for plain Python literals — Scalar's implicitly_convertible registrations only take effect in pybind11's convert pass).
  • Storage: same treatment for fill/append/from_pylist. from_pylist had several live bugs, found across multiple review passes:
    • Storage.from_pylist([True, False]) silently produced Uint64 instead of Bool (Python bool is an int subclass, and from_vector was registered after the uint64 overload). Fixed by reordering.
    • Storage.from_pylist([np.int32(2)]) (and other numpy scalar dtypes narrower than int64/uint64/float64/complex128) silently produced a Bool storage holding True instead of the numeric value — numpy integer/bool scalars aren't subclasses of Python int/bool, so they matched nothing in pybind11's no-convert pass and fell through to the Bool overload's convert-pass truthiness fallback. Fixed by adding a numpy_scalar keep-set ahead of the Bool overload, and making the Bool overload itself strict (std::vector<py::bool_>, no truthiness fallback).
    • Storage.from_pylist([np.int8(5)]) produced Double instead of an integer dtype (cytnx has no Int8/Uint8; the new keep-set had no entry for them). Fixed by widening to Int16/Uint16.
    • Storage.from_pylist([])'s dtype default is now handled by an explicit pylist.empty() check in the first-matching overload's body, matching numpy/the Python array API's own convention (np.array([]).dtype == float64) — a runtime-only change that doesn't touch any stub-visible signature. (Reordering the overload set to control the empty-list winner was tried first and reverted: it conflicts with keeping complex128 registered last, which mypy.stubtest requires.)
  • LinOp.set_elem: same duplicate-set cleanup; the numpy_scalar block must precede py::int_/double/complex128, matching every other keep-set in the codebase.
  • Scalar.__init__: dropped the separate uint64/int64 raw constructors (which rendered as an identical stub signature) for a single py::int_ overload via dispatch_pyint. This is also a live bug fix: those raw constructors were intercepting every numpy int scalar narrower than int64 (np.uint64/int32/uint32/int16/uint16) via the same no-convert trap, so e.g. Scalar(np.uint64(5)).dtype() was Int64 instead of Uint64 — this incidentally closes out the same behavior a previously-approved-but-superseded PR (fix(pybind): keep-set ordering for Scalar constructors, add __bool__ #1014) targeted. Scalar(5).dtype() also changes from Uint64 to Int64 (magnitude-based dispatch, matching every other keep-set's int64-preferred convention). Scalar(np.int8(5)) had the same "no Int8 dtype" gap as from_pylist above (was Double, now widens to Int16). Note: Scalar's Python binding is slated for eventual removal (Remove the cytnx.Scalar Python binding (Python should derive type from dtype) #1045, not yet started) per the maintainers' direction in Design question: should scalars be rank-0 (shape-{}) tensors? #1016 — these fixes have interim value and don't block that removal.
  • UniTensor.get_block/get_block_: collapsed the qnum parameter's separate vector/vector overloads (identical stub signature) into one vector overload via a new dispatch_pyint_vector helper. This introduced its own regression: the removed raw vector<cytnx_int64>/vector<cytnx_uint64> overloads accepted numpy integer scalars through pybind11's arithmetic-type __index__ path, but the replacement py::int_-based dispatcher only accepts actual Python intget_block([np.int64(0), np.int64(0)], False) regressed from working to raising TypeError. Fixed by adding a numpy_scalar keep-set (int64/uint64/int32/uint32/int16/uint16) ahead of the py::int_ dispatcher, for both the labeled and unlabeled forms.
  • Tensor.__radd__/__rsub__/__rmul__/__rtruediv__/__rfloordiv__/__rmod__: removed the numpy_scalar-typed overloads entirely. Tensor defines __iter__, so numpy's ufunc dispatch machinery treats a numpy-scalar-on-the-left operand as array-like and tries to iterate it before ever falling back to these reverse operators, raising TypeError: 'TensorIterator' object is not iterable regardless of dtype (issue Unexpected dtype returned when operating with Float32 / ComplexFloat32 tensors and numpy scalars #692) — confirmed empirically across int64/complex64/uint16/bool, so the overloads were dead code and also the source of mypy.stubtest [misc] "Forward operator not callable" errors under Python 3.10's numpy 2.2.6 (a numpy typeshed limitation, not fixable from cytnx's side). UniTensor's equivalent reverse operators are reachable and untouched.
  • A handful of pure duplicate registrations (Storage.__len__, Symmetry.clone, UniTensor.get_blocks_) were dropped.
  • tools/generate_stubs.py: added a complex | prefix to the SupportsComplex | SupportsFloat | SupportsIndex union pybind11 3.0.4 emits for cytnx_complex128 parameters — typing.SupportsComplex alone rejects a plain Python complex literal since complex has no __complex__.

Regenerated cytnx/cytnx/*.pyi (Python 3.10, per CONTRIBUTING.md's recipe) against all of the above, including a rebase onto #915's own ExpH/ExpM fix (a dedicated binding-level ComplexArg type_caster in pybind/complex_arg.hpp, added after this PR branched).

Scope notes:

Testing

  • python -m mypy.stubtest cytnx.cytnx (Python 3.10): 0 overload-cannot-match errors (down from 768). Remaining [misc] (73), [assignment] (5), [override] (7) errors are pre-existing pybind11-stubgen __eq__/__hash__ Liskov-substitution boilerplate, unrelated to this PR.
  • ctest --test-dir build/debug-openblas-cpu / build/debug-mkl-cpu: 1767/1768 passed both presets — the one failure (BlockUniTensorTest.CombineBondForceDoesNotRewriteUserHeldBond) is a pre-existing, unrelated ASan memcpy-param-overlap bug in src/BlockUniTensor.cpp/src/utils/vec_map.cpp (this PR touches no files under src/, include/, or tests/), filed as ASan memcpy-param-overlap in BlockUniTensor::permute_ (via combineBond), CombineBondForceDoesNotRewriteUserHeldBond #1052.
  • pytest pytests/: 288 passed, 1 skipped (pre-existing, environment-dependent graphviz skip).
  • Added pytests/keep_set_dispatch_test.py for the dtype-selection bugs fixed here (Storage.from_pylist's bool/int/numpy-scalar/8-bit/empty-list dispatch, Scalar's plain-int and 8-bit constructor dispatch, LinOp.set_elem's numpy-scalar dispatch, UniTensor.get_block's qindex dispatch including numpy integer widths, negative-overflow rejection), and flipped pytests/scalar_variant_test.py's narrower-int constructor test from pinning the old buggy behavior to asserting the fixed one.

Closes out the remaining scope of #928 (stub-quality fixes at the binding layer instead of generate_stubs.py post-processing) for the overload-cannot-match category specifically.

@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 consolidates duplicate per-dtype overloads into unified 'keep-sets' across several pybind files (including LinOp, Scalar, Storage, Tensor, and UniTensor), resolving binding-level bugs and type-stub mismatches. The review feedback correctly identifies a critical bug in both dispatch_pyint_vector and from_pylist where negative integer overflows are incorrectly treated as requiring uint64_t conversion instead of triggering an out-of-range error, which would lead to invalid casts.

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 pybind/unitensor_py.cpp
Comment thread pybind/storage_py.cpp
Comment thread tools/generate_stubs.py
Comment thread pytests/keep_set_dispatch_test.py Outdated
Comment thread pytests/keep_set_dispatch_test.py Outdated
Comment thread pytests/keep_set_dispatch_test.py Outdated
IvanaGyro and others added 11 commits July 15, 2026 13:42
…loads

pyint_dispatch.hpp's KEEP-SET ORDERING documents cytnx::Scalar as always
registered last in each scalar-accepting operator's overload set. Scalar
defines __float__/__complex__, so it structurally satisfies the
typing.SupportsFloat and typing.SupportsComplex protocols that pybind11
3.0.4 renders for the cytnx_double/cytnx_complex128 overloads; with
Scalar registered after those, mypy.stubtest correctly reports the
Scalar overload as unreachable (overload-cannot-match), since any
Scalar argument already matches an earlier, structurally broader
overload in the stub's declared order.

Move Scalar's overload to register immediately after py::int_, before
cytnx_double/cytnx_complex128, in every scalar-accepting operator on
Tensor and UniTensor. This only affects declaration order for pybind11's
own overload resolution, which is independent of the mypy-visible order
problem: pybind11 resolves a call in two passes (exact-type match, then
conversion), and a plain Python float/complex/int is an exact match for
cytnx_double/cytnx_complex128/py::int_'s casters in the first pass
regardless of where Scalar sits, so Scalar's own implicit-conversion
registrations (py::implicitly_convertible<cytnx_double, Scalar>() and
the complex128 counterpart in scalar_py.cpp) are never reached for those
arguments. Verified directly: float/complex/int/numpy-scalar/Scalar
operands on __add__/__radd__/__sub__/__rsub__/__mul__/__rmul__/
__truediv__/__eq__ (Tensor) and __add__/__radd__/__iadd__ (UniTensor)
all produce identical dtype and value before and after the reorder, and
pytests/binding_dtype_test.py passes unchanged (18/20; the other 2 hit
an unrelated pre-existing ASan __cxa_throw interceptor issue on
exception-raising __bool__ tests, reproducible on the documented build
workflow independent of this change).

Co-Authored-By: Claude <noreply@anthropic.com>
Two gaps left over from the earlier Scalar-reorder pass and #915's
keep-set consolidation, both still on Tensor:

- fill/append were still bound once per raw C++ dtype (complex128,
  complex64, double, float, int64, uint64, int32, uint32, int16,
  uint16, bool), duplicating the same overload-cannot-match problem
  #915/ecb1fbd already fixed for the arithmetic operators. Replaced
  with the same numpy_scalar/py::int_/double/complex128 keep-set.
- __iadd__, __isub__, __imul__, __itruediv__, __ifloordiv__, __ne__,
  and __setitem__ still had their Scalar overload registered after
  cytnx_double/cytnx_complex128, unreachable in the stub for the same
  structural-subtyping reason as the operators already fixed (Scalar
  defines __float__/__complex__, so it satisfies the SupportsFloat/
  SupportsComplex protocols those overloads render as). These use a
  py::object self + multi-line lambda body shape that the original
  scan (looking for `[]` immediately after the comma) didn't match, so
  they were missed. Reordered Scalar to register right after
  py::int_, before cytnx_double/cytnx_complex128, matching every other
  operator.

Co-Authored-By: Claude <noreply@anthropic.com>
Storage.fill/append, LinOp.set_elem, and Scalar's constructor each had
two coexisting registration styles: a raw per-C++-dtype set (complex128,
complex64, double, float, int64, uint64, int32, uint32, int16, uint16,
bool) and, for LinOp/Scalar, an already-correct numpy_scalar-wrapped
set added later but never cleaned up. Replaced with a single keep-set
per method, dropping the narrow-width duplicates the numpy_scalar
overloads already cover.

The numpy_scalar block must be registered before any plain integral/
double/complex128 overload (see "KEEP-SET ORDERING" in
pybind/pyint_dispatch.hpp): a numpy scalar satisfies __index__/
__float__/__complex__, so an earlier plain overload can consume it in
pybind11's conversion pass for any numpy dtype the keep-set omits (e.g.
float16, which none of these three keep-sets register a numpy_scalar
overload for). LinOp.set_elem and Scalar's raw registrations were
ordered the other way around; reordered for consistency with every
other keep-set in this codebase, not just to remove the stub-visible
duplicate. LinOp.set_elem's target assignment (`x(0) = elem`, via
Tensor::Tproxy::operator=) tolerates a complex-into-real narrowing that
Tensor's own set<T>()/__setitem__ path rejects, so no live crash was
reproduced for LinOp specifically -- this is a defensive consistency
fix, not a confirmed bug fix.

Scalar's constructor drops separate uint64/int64 overloads (which
rendered as the identical stub annotation regardless of order, so one
was always flagged unreachable even though a small positive value vs.
a negative one previously exercised each of them respectively, purely
from uint64 being registered first) in favor of a single py::int_
overload with dispatch_pyint. This adopts dispatch_pyint's int64-
preferred convention, already used by every other keep-set in this
codebase, for consistency; Scalar(5).dtype() changes from Uint64 to
Int64.

Storage.from_pylist gets the same treatment plus a fix for a live bug:
Python bool is an int subclass, and with Storage.from_vector<cytnx_bool>
registered after from_vector<cytnx_uint64> (its previous position),
Storage.from_pylist([True, False]) silently produced a Uint64-dtype
Storage instead of Bool. Reordering bool first, and moving double ahead
of complex128 for the same covariant-Sequence reason as the numpy
scalars (Sequence[SupportsFloat | SupportsIndex] is a subtype of
Sequence[SupportsComplex | SupportsFloat | SupportsIndex]), fixes the
reachability rather than just the stub duplicate. The int-list case
becomes a single py::sequence overload that inspects every element's
magnitude and picks int64 unless any element needs uint64's range,
matching dispatch_pyint's convention (Storage.from_pylist([1, 2, 3])
was Uint64, is now Int64) instead of two overloads that rendered as
the identical stub annotation.

Storage.__len__ was registered twice, identically; dropped the
duplicate.

Co-Authored-By: Claude <noreply@anthropic.com>
Helpclass.set_elem (the UniTensor.at() proxy's element setter) had the
same raw-per-C++-dtype-plus-unused-numpy_scalar-block duplication as
Storage/LinOp's set_elem/fill/append, fixed the same way.

UniTensor.get_block/get_block_ each had separate vector<int64>/
vector<uint64> qnum overloads, which rendered as the identical stub
annotation regardless of registration order (same issue as
Storage.from_pylist's int-list case), so one was always flagged
overload-cannot-match even though both were reachable depending on
list content. Replaced with a single py::sequence overload using a new
dispatch_pyint_vector helper (the vector counterpart of
pyint_dispatch.hpp's dispatch_pyint), which picks int64 unless any
element needs uint64's range.

Symmetry.clone and UniTensor.get_blocks_ were each registered twice,
identically; dropped the duplicates.

Co-Authored-By: Claude <noreply@anthropic.com>
The comments added for LinOp.set_elem, Scalar's constructor,
Storage.from_pylist, and UniTensor's dispatch_pyint_vector described what
the previous registration produced ("was Uint64, is now Int64", "matched
the raw complex128 overload... before this change"). That narrative
belongs in the commit message, not in a comment describing the code as it
stands; a comment describing a prior state ages badly once nobody
remembers what "previously" refers to. Trimmed each comment to describe
only the current registration order and why it matters, and let
clang-format correct dispatch_pyint_vector's indentation inside its
enclosing anonymous namespace while the file was open.
Tensor defines __iter__, so numpy's ufunc dispatch machinery treats a
numpy-scalar-on-the-left operand (e.g. np.float32(1.0) + t) as
array-like and tries to iterate it before ever falling back to
__radd__/__rsub__/__rmul__/__rtruediv__/__rfloordiv__/__rmod__,
raising "TypeError: 'TensorIterator' object is not iterable" (issue
#692) regardless of numpy scalar dtype -- confirmed empirically across
int64, complex64, uint16, and bool. The numpy_scalar<...>-typed
overloads registered for these six operators were therefore never
reachable, and mypy.stubtest flags them as [misc] "Forward operator
not callable" under Python 3.10's numpy 2.2.6 (numpy's own typeshed
limitation, not fixable from cytnx's side). Removed the nine
numpy_scalar overloads per operator; the py::int_/Scalar/double/
complex128 overloads are unaffected, since a plain Python operand
never goes through numpy's ufunc dispatch and the stub for those
still matches reality.

UniTensor's equivalent reverse operators are reachable (confirmed
empirically for np.int64/np.float32 on the left) and are not touched.

Co-Authored-By: Claude <noreply@anthropic.com>
Covers the dtype-selection bugs fixed by consolidating per-C++-dtype
duplicate pybind11 overloads into keep-sets: Storage.from_pylist
mis-selecting Uint64 instead of Bool for a list of Python bools (bool
is an int subclass) and preferring Uint64 over Int64 for a plain
non-negative int list purely from registration order; Scalar's
plain-int constructor's analogous Uint64/Int64 tie-break. Also adds
light reachability coverage for UniTensor.get_block/get_block_'s
collapsed qnum-list overload, LinOp.set_elem's numpy-scalar keep-set,
and Tensor's __r*__ operators (numpy-scalar-on-the-left still raises
the same TypeError as before; the remaining py::int_/Scalar/double/
complex128 overloads still work).

Co-Authored-By: Claude <noreply@anthropic.com>
Storage/LinOp/Scalar's keep-set consolidation dropped Scalar's raw
per-C++-dtype py::init<>() constructors (cytnx_int64, cytnx_uint64,
cytnx_int32, ...) in favor of the numpy_scalar-typed overloads plus a
single py::int_ overload. Those raw constructors were the cause of a
documented pre-existing quirk: a numpy scalar narrower than int64/
uint64 (np.uint64/int32/uint32/int16/uint16) always resolved to the
raw Int64 constructor instead of its own numpy_scalar<T> overload,
via the same "__index__ no-convert trap" documented under "KEEP-SET
ORDERING" in pybind/pyint_dispatch.hpp. With the raw constructors
gone, each of these five numpy dtypes now reaches its own overload
and constructs a Scalar of the matching dtype (confirmed empirically:
Scalar(np.uint64(5)).dtype() is now Uint64, not Int64, and likewise
for int32/uint32/int16/uint16).

Flips test_numpy_scalar_constructor_paths_narrower_ints_preexisting_
quirk from pinning the old Int64-for-everything behavior to asserting
each numpy dtype's own correct dtype, and drops the now-stale
"astype() is the reliable way to pin an unsigned dtype" rationale from
test_inplace_signed_unsigned_mix_promotes_to_signed's comment (the
direct numpy constructor now works, though astype() is kept for
consistency with the surrounding tests in this file).

This incidentally closes out the same behavior PR #1014 targeted
(reviewed and approved, but closed as superseded once #1016 decided
Scalar's Python binding should eventually be removed entirely,
tracked in #1045 -- not yet started, so this fix has interim value).

Co-Authored-By: Claude <noreply@anthropic.com>
Storage.from_pylist's int-list dispatch and UniTensor's
dispatch_pyint_vector (used by get_block/get_block_'s qnum parameter)
both used PyLong_AsLongLongAndOverflow's overflow flag to decide
between an int64 and a uint64 kernel, treating any nonzero overflow
the same way. PyLong_AsLongLongAndOverflow sets overflow to -1 for a
value below LLONG_MIN, not just +1 for a value above LLONG_MAX -- a
value that negative does not fit in uint64_t either (uint64_t cannot
represent any negative value), so routing it into the uint64 branch
led to a failed cast surfacing as a generic pybind11 RuntimeError
instead of a clean domain error.

Split the check: overflow < 0 raises immediately via cytnx_error_msg,
overflow > 0 sets the uint64 flag, matching pyint_dispatch.hpp's
single-value dispatch_pyint (which already handles this correctly via
PyLong_AsUnsignedLongLong's own OverflowError). Also stopped breaking
out of the scan on the first overflow, so a negative overflow later in
a list is still caught even if an earlier element needed uint64.

Co-Authored-By: Claude <noreply@anthropic.com>
Three fixes to review comments on this file:

- The negative-overflow tests for Storage.from_pylist and
  UniTensor.get_block only asserted on the exception type
  (cytnx.CytnxError), which any unrelated validation error (e.g. a
  qnum mismatch) would also satisfy, so the tests did not actually
  discriminate the overflow-specific code path from other failure
  modes. Added match= on the specific "out of the supported
  int64/uint64 range" message.

- test_linop_set_elem_numpy_scalar_dtypes asserted a computed value
  that would still match even if np.float32(1.5) were silently routed
  through the wrong (complex128) overload -- LinOp.set_elem's target
  assignment tolerates that without raising or changing the resulting
  value, so no assertion on the result can distinguish which overload
  matched. Renamed to test_linop_set_elem_numpy_scalar_dtypes_are_
  accepted and reworded the section comment to state plainly that this
  is reachability coverage only, not a discriminating regression test.

- test_tensor_numpy_scalar_on_left_still_raises_typeerror pinned
  issue #692's still-unreachable numpy-scalar-on-the-left behavior as
  an expected TypeError. #692 tracks this as a genuine bug, not a
  design decision, and a test asserting a known bug's current
  symptom needs updating (and could be forgotten) the moment #692 is
  fixed. Removed it; the regression coverage that matters for this
  PR is that the kept overloads still work, which
  test_tensor_plain_python_and_scalar_reverse_ops_still_work already
  covers.

Co-Authored-By: Claude <noreply@anthropic.com>
Fresh python tools/generate_stubs.py run against the fully-rebased
source tree (fix/overload-cannot-match-remaining replayed onto
claude/fix-stubtest-errors), superseding the two stub-regeneration
commits from before the rebase whose generated content was superseded
by intervening changes on the new base.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the fix/overload-cannot-match-remaining branch from 70b0e59 to 1df8cbe Compare July 15, 2026 14:00
@IvanaGyro
IvanaGyro marked this pull request as ready for review July 15, 2026 14:02

@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: 1df8cbe608

ℹ️ 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 pybind/storage_py.cpp Outdated
from_pylist's Bool overload bound Storage::from_vector<cytnx_bool>
directly, which pybind11 exposes through the arithmetic bool
type_caster. That caster's convert-pass falls back to
PyObject_IsTrue() for any object, so a list of numpy scalar elements
narrower than int64/uint64/float64/complex128 (e.g. np.int32, np.uint16,
np.float32) matched nothing in the no-convert pass and then matched the
Bool overload's convert pass by truthiness -- from_pylist([np.int32(2)])
silently produced a Bool-dtype Storage holding True instead of an Int32
value of 2.

Fixes this two ways:

- Add a numpy_scalar keep-set (complex64/float/int64/uint64/int32/
  uint32/int16/uint16/bool) ahead of the Bool overload, per the
  KEEP-SET ORDERING convention in pyint_dispatch.hpp, so these numpy
  dtypes match their own overload in the no-convert pass before the
  Bool overload's convert pass is ever attempted.
- Replace the Bool overload's binding with a lambda over
  std::vector<py::bool_>. py::bool_'s caster does a strict isinstance
  check with no truthiness-based convert-pass fallback (unlike the
  arithmetic bool caster), so any remaining non-bool input now falls
  through to "no matching overload" instead of being silently
  misclassified.

A numpy scalar type narrower than float64/complex128 that has no
cytnx-backed dtype at all (e.g. np.float16, np.int8) still widens to
Double via the existing double overload, matching that overload's
existing catch-all role documented in the keep-set comment -- it does
not raise.
Covers the dtype and value produced by from_pylist for each of the
numpy scalar types that previously fell through to the Bool overload's
truthiness-based misclassification (int64/uint64/int32/uint32/int16/
uint16/float32/complex64/bool).
Adds the 9 new from_pylist overloads to __init__.pyi. Also corrects
linalg.pyi's ExpH/ExpM b parameter default from the elided "..." repr
back to "0j" -- the prior stub-regeneration commit ran against a stale
build/debug-openblas-cpu-venv .so that predated the ComplexArg fix,
so it didn't pick up the corrected default repr despite the C++ side
already having it.

@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: 76e6f4246b

ℹ️ 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 pybind/unitensor_py.cpp
The qnum consolidation replaced get_block/get_block_'s raw
vector<cytnx_int64>/vector<cytnx_uint64> overloads with a single
std::vector<py::int_> dispatcher (dispatch_pyint_vector). The removed
overloads used pybind11's arithmetic integral caster, which accepts
any object satisfying __index__ (numpy integer scalars included) even
in the no-convert pass; py::int_'s caster does a strict isinstance(int)
check with no such fallback, so numpy integers are not Python int
subclasses and matched neither overload. get_block([np.int64(0),
np.int64(0)], False) went from working to raising TypeError.

Adds a dispatch_numpy_int_vector helper and a numpy_scalar keep-set
(int64/uint64/int32/uint32/int16/uint16) ahead of the py::int_
dispatcher, for both the labeled and unlabeled get_block/get_block_
forms, mirroring the same fix already applied to Storage.from_pylist.
Covers the labeled and unlabeled get_block/get_block_ forms across
the six numpy integer widths that previously stopped matching after
the qnum consolidation replaced their raw vector<cytnx_int64>/
vector<cytnx_uint64> overloads with a stricter py::int_ dispatcher.
Adds the 24 new get_block/get_block_ overloads (4 call sites x 6
numpy integer widths) to __init__.pyi.
get_block/get_block_'s vector argument is the per-leg quantum-number
sector index, matched against BlockUniTensor's _inner_to_outer_idx
after an unsigned cast (include/UniTensor.hpp: "this one for Block
will return the indicies!!"), not the physical charge value. The test
names and one comment claimed otherwise ("U1 does support negative
charges physically"), which doesn't apply to this API -- get_block
never receives a charge value in the first place. Renamed the
qnum-suffixed test names and qnum-named locals to qindex, and
rewrote the negative-overflow test's comment to describe it as a
mechanical dispatch_pyint_vector check rather than a claim about
negative charges.

The public py::arg("qnum") name in pybind/unitensor_py.cpp is
unchanged -- it predates this PR and renaming a public keyword
argument is a separate, breaking API change.

@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: 25166e02b1

ℹ️ 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 pybind/scalar_py.cpp
cytnx has no Int8/Uint8 dtype, so the numpy_scalar keep-set added to
Scalar's constructor had no int8/uint8 entry. Without one ahead of the
raw cytnx_double constructor, np.int8/np.uint8 fell through to it --
Scalar(np.int8(5)).dtype() silently became Double instead of an
integer dtype. Before this PR's constructor consolidation, these
values were incidentally caught by a raw arithmetic-typed constructor
registered ahead of the numpy_scalar block (pybind11's arithmetic
integral caster accepts anything with __index__ even in the
no-convert pass), landing on some integer dtype; this made the
regression floating-point where it used to be integer, not merely a
different integer width.

Widens to Int16/Uint16, the narrowest integer dtype cytnx supports,
rather than a real-valued dtype.

The same numpy_scalar<int16_t>/<uint16_t> keep-set pattern (with no
int8/uint8 counterpart) appears in ~90 other sites across
tensor_py.cpp, unitensor_py.cpp, storage_py.cpp, and linop_py.cpp;
fixing all of them is out of scope here and tracked in #1065.
Covers dtype and sign for np.int8/np.uint8 constructor arguments,
which previously fell through to Scalar's raw cytnx_double
constructor and produced a floating-point dtype instead of an integer
one.
Adds the 2 new Scalar.__init__ overloads to __init__.pyi.

@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: fb0cb2b117

ℹ️ 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 pybind/storage_py.cpp
Comment thread pybind/storage_py.cpp
Same gap as Scalar's constructor (#1053): cytnx has no Int8/Uint8
dtype, so from_pylist's numpy_scalar keep-set had no int8/uint8 entry
and those inputs fell through to the double overload --
from_pylist([np.int8(5)]) produced a Double storage instead of an
integer one. Widens to Int16/Uint16, matching the Scalar fix.

Also documents (without attempting to fix by reordering) that
from_pylist([]) changed its default dtype from ComplexDouble to
ComplexFloat as a side effect of this keep-set's numpy_scalar-first
ordering: an empty list has no elements for any vector<T> caster to
check, so every from_pylist overload matches it equally and the
first-registered one wins by construction. Moving complex128 first to
preserve the old default was tried and reverted -- mypy's overload
checker has no notion of pybind11's runtime no-convert/convert
passes, so an earlier "complex" stub signature unconditionally shadows
every later, narrower signature, reintroducing 14 overload-cannot-match
errors (exactly the class of bug this PR fixes). No overload ordering
satisfies both constraints (numpy_scalar-first for non-empty-list
correctness, complex128-first for the old empty-list default)
simultaneously under pybind11's per-dtype-overload model.
… dtype

Covers dtype and value for np.int8/np.uint8 elements (previously fell
through to Double), and pins from_pylist([])'s new ComplexFloat
default (previously ComplexDouble) as a regression test for the
default itself -- see storage_py.cpp's from_pylist comment for why no
overload ordering can restore the old default without breaking
mypy.stubtest.
Adds the 2 new from_pylist overloads to __init__.pyi.

@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: d6ca1ee74c

ℹ️ 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 pybind/storage_py.cpp
py::arg("pylist"), py::arg("device") = (int)cytnx::Device.cpu)
.def_static(
"from_pylist",
[](const std::vector<py::int_> &pylist, const int &device) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle mixed Python/NumPy int lists before float fallback

Fresh evidence beyond the prior homogeneous NumPy-scalar fixes: a sequence like Storage.from_pylist([np.int32(2), 3]) has no integer overload that can accept every element, because the exact std::vector<py::numpy_scalar<int32_t>> overload above rejects the Python int and this std::vector<py::int_> overload rejects the NumPy scalar. The call then reaches the cytnx_double vector overload and creates a Double storage, whereas the removed raw integral vector overloads accepted both values via __index__. Please dispatch integer sequences element-by-element, or otherwise accept generic __index__ values, before the double/complex fallbacks.

Useful? React with 👍 / 👎.

Comment thread pybind/unitensor_py.cpp
[](UniTensor &self, const std::vector<std::string> &labels, const std::vector<cytnx_int64> &qnum, const bool &force) {
return self.get_block_(labels, qnum, force);
.def("get_block",
[](const UniTensor &self, const std::vector<py::int_> &qnum, const bool &force) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve mixed NumPy/Python qindex sequences

Fresh evidence beyond the prior homogeneous NumPy qindex fix: get_block([np.int32(0), 0], False) still has no matching overload, since the exact std::vector<py::numpy_scalar<int32_t>> overload above rejects the Python literal and this std::vector<py::int_> overload rejects the NumPy scalar. The removed vector<cytnx_int64>/vector<cytnx_uint64> overloads accepted mixed __index__ values, so callers that combine NumPy qindices with Python literals now get TypeError; dispatch the qnum sequence element-by-element or accept a generic __index__ sequence.

Useful? React with 👍 / 👎.

An empty Python list has no elements for any of from_pylist's
vector<T> overloads to check, so pybind11's no-convert pass accepts
it for every one of them equally and the first-registered overload
wins regardless of T -- from_pylist([]) was at the mercy of
registration order alone, and produced ComplexFloat.

Reordering the overload set to control which one wins the empty case
was tried (see the previous commit's from_pylist comment) and
reverted: it conflicts with keeping complex128 registered last, which
mypy.stubtest requires.

Handled instead with an explicit pylist.empty() check inside the
first-registered overload's body. This only changes the function's
runtime behavior, not its declared parameter type, so it doesn't
touch the stub-visible signature or interact with mypy.stubtest at
all. Routes to Double, matching numpy/the Python array API's own
empty-array, no-explicit-dtype default (np.array([]).dtype ==
float64) instead of the arbitrary ComplexFloat this keep-set's
ordering happened to produce.
…ault

Updates the empty-list regression test for the previous commit's fix
(was pinned to the arbitrary ComplexFloat the keep-set's registration
order happened to produce; now pinned to Double, matching
np.array([]).dtype).
@IvanaGyro

Copy link
Copy Markdown
Member Author

I am tired on keeping this PR on tip of the master branch. I have maintained this stub change for weeks. During these weeks, many PRs merged into master, which cause me to update the branch.

Please take over this PR and the following PRs. I am stopping my AI bot tracing on these PRs.

@yingjerkao

Copy link
Copy Markdown
Collaborator

I am tired on keeping this PR on tip of the master branch. I have maintained this stub change for weeks. During these weeks, many PRs merged into master, which cause me to update the branch.

Please take over this PR and the following PRs. I am stopping my AI bot tracing on these PRs.

I will merge this PR but I suggest that we postpone the stub update until we stabilize the code base.

Base automatically changed from claude/fix-stubtest-errors to master July 17, 2026 06:12
Brings #1053 up to date with master after #915 landed. Only two files
conflicted:

- pybind/tensor_py.cpp: master (#941) deliberately UNBINDS Tensor's
  __floordiv__/__rfloordiv__/__ifloordiv__ (binding them to Div would make
  `t // x` silently perform true division). Master's own comment states this
  "supersedes the P1-T2/T3 keep-set treatment these operator groups
  received" -- i.e. it directly supersedes #1053's floordiv keep-set.
  Resolved by taking master's unbind; #1053's keep-set consolidation for all
  other operators (add/sub/mul/truediv/mod/eq, plus Storage from_pylist and
  UniTensor/Scalar/LinOp/Symmetry) is preserved. #1053's keep-set tests do
  not exercise floordiv, so nothing of #1053 breaks.

- cytnx/cytnx/__init__.pyi: regenerated from the merged bindings via
  tools/generate_stubs.py (the correct resolution for generated output),
  reverting only a machine-specific Device.pyi Ncpus value.

Testing (openblas-cpu): pycytnx builds; mypy.stubtest reports 0
overload-cannot-match (down from 535 pre-#1053), leaving only the pre-
existing 7 override + 5 assignment __eq__/__hash__ warnings (a separate,
out-of-scope class); pytest pytests/ = 308 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@yingjerkao
yingjerkao merged commit 9522995 into master Jul 17, 2026
13 checks passed
@yingjerkao
yingjerkao deleted the fix/overload-cannot-match-remaining branch July 17, 2026 06:27

@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: 61b7aa5ffd

ℹ️ 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 pybind/unitensor_py.cpp
Comment on lines +1245 to +1249
"get_block",
[](const UniTensor &self, const std::vector<py::numpy_scalar<uint16_t>> &qnum,
const bool &force) {
return dispatch_numpy_int_vector(qnum,
[&](const auto &v) { return self.get_block(v, force); });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Accept 8-bit NumPy qindex arrays

When callers pass qindices as np.int8/np.uint8 scalars or arrays, they no longer match any of these homogeneous py::numpy_scalar<...> overloads, and the std::vector<py::int_> fallback below rejects NumPy scalar elements. The removed raw vector<cytnx_int64>/vector<cytnx_uint64> overloads accepted those values via __index__, so e.g. a small np.array([0, 0], dtype=np.int8) now raises TypeError for get_block/get_block_ instead of selecting the block; add int8/uint8 qindex handlers before the py::int_ fallback.

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.17179% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.64%. Comparing base (09bfcb9) to head (61b7aa5).
⚠️ Report is 32 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
pybind/unitensor_py.cpp 81.06% 39 Missing ⚠️
pybind/storage_py.cpp 89.95% 21 Missing ⚠️
pybind/tensor_py.cpp 81.89% 21 Missing ⚠️
pybind/linop_py.cpp 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1053      +/-   ##
==========================================
+ Coverage   72.29%   72.64%   +0.35%     
==========================================
  Files         226      226              
  Lines       27944    28184     +240     
  Branches       71       71              
==========================================
+ Hits        20202    20475     +273     
+ Misses       7721     7688      -33     
  Partials       21       21              
Flag Coverage Δ
cpp 72.76% <85.17%> (+0.35%) ⬆️
python 64.13% <ø> (ø)

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

Components Coverage Δ
C++ backend 71.85% <ø> (+0.19%) ⬆️
Python bindings 77.70% <85.17%> (+1.01%) ⬆️
Python package 64.13% <ø> (ø)
Files with missing lines Coverage Δ
pybind/scalar_py.cpp 98.44% <100.00%> (+7.17%) ⬆️
pybind/symmetry_py.cpp 96.77% <ø> (-0.06%) ⬇️
pybind/linop_py.cpp 83.33% <87.50%> (-0.37%) ⬇️
pybind/storage_py.cpp 84.73% <89.95%> (+2.87%) ⬆️
pybind/tensor_py.cpp 71.37% <81.89%> (+0.65%) ⬆️
pybind/unitensor_py.cpp 74.39% <81.06%> (+0.51%) ⬆️

... 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 09bfcb9...61b7aa5. 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.

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.

3 participants