* Match invalid format specifier error details
Assisted-by: Codex:gpt-5.6-sol
* format: convert FormatSpecError in one helper
InvalidFormatSpecifier needs the spec and the object type. Drop
IntoPyException so that path cannot produce a generic message.
Assisted-by: Grok:4.6
---------
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
* Give dict a CPython-style message for unhashable keys
Dict operations (subscription, membership, get/pop/setdefault, ...) now
raise "cannot use 'X' as a dict key (unhashable type: 'X')", matching
CPython 3.14 and mirroring the existing PySetInner wrapping. The key is
only materialized on the error path, so hashable keys pay no extra cost.
Enables Lib/test/test_dict.py::DictTest::test_unhashable_key.
Assisted-by: Claude Opus 4.8 (Anthropic)
* dict: only rewrite hashing failures, not key-comparison errors
wrap_unhashable_error previously rewrote any TypeError from a dict
operation as an unhashable-key error, but those operations also compare
keys on a hash collision, so a TypeError from a colliding key's __eq__
was mislabeled. Disambiguate on the error path by re-hashing the key, so
the successful path still hashes exactly once (keeps the do-not-rehash /
atomic invariants) while comparison errors propagate unchanged.
Adds a colliding-key regression test.
Assisted-by: Claude Opus 4.8 (Anthropic)
* dict: hash unhashable keys once up front and cover every insertion path
Addresses the review feedback (thanks @luantaraschi):
- The message now reaches update(), the constructor and |= as well, not just
__setitem__: merge_object_with_override and merge_from_seq2 now hash the key
up front and thread it into contains_known_hash/insert_known_hash.
- Hash the key once instead of re-hashing on the error path. This threads the
hash through the *_known_hash operations (add dict_inner get_known_hash), so
a __hash__ that fails only on its first call is still reported instead of
escaping unwrapped.
- Use the fully-qualified type name (matching CPython's %T).
- Only an exact TypeError is rewritten; a __hash__ raising a TypeError subclass
now propagates unchanged.
pop() and setdefault() have no *_known_hash entry point on the inner map, so
they hash once up front for the message and then do their own single lookup.
Assisted-by: Claude Opus 4.8
* dict: thread the known hash through setdefault and pop as well
Follow-up on the review: setdefault() and pop() previously hashed the key a
second time (inside the inner map) after hash_or_unhashable already hashed it,
so a stateful __hash__ failing on the second call would skip the dict-specific
message. Add setdefault_known_hash / pop_known_hash to the inner map and thread
the precomputed hash, so every operation now hashes exactly once. Adds a
regression test asserting setdefault/pop call __hash__ only once.
Assisted-by: Claude Opus 4.8
* test: hash the key on a non-empty dict so CPython 3.14 exercises the pop path
`{}.pop(CountingHash())` on an empty dict does not hash the key under
CPython 3.14 (the lookup short-circuits), so `CountingHash.calls == 1` was 0
and the snippet failed its CPython parity run. Use a non-empty dict so the
lookup must hash the key exactly once, on both CPython and RustPython. Also
applied `ruff format` (two blank lines before top-level defs).
Assisted-by: Claude Opus 4.8 (Anthropic)
* dict: drop unused internal pop wrapper
Per review, the vm-taking Dict::pop wrapper was dead code (#[allow(dead_code)],
kept only for API symmetry) with no callers. Remove it and fold its doc into
pop_known_hash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* dict: rename pop_known_hash to pop
Per review, with the plain pop gone the _known_hash suffix no longer earns
its keep, so drop it. Matches the remaining known-hash helpers that still
pair with a base method.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* dict: fold setdefault wrapper into setdefault, drop unused setdefault_entry
Per review, apply the same pattern as pop: the dead setdefault wrapper only
hashed and delegated, so remove it and rename setdefault_known_hash to
setdefault (callers already thread the hash). Also drop setdefault_entry,
which this PR added but nothing uses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* dict: use set_cause and Result from fully_qualified_name
After rebasing onto main, fully_qualified_name returns PyResult
and set___cause__ is a private Python setter.
Assisted-by: Grok:4.6
* dict_inner: take a hash for every key operation
Drop the hashing wrappers and *_known_hash aliases. Callers hash
the key and pass it in.
Assisted-by: Grok:4.6
* snippets: add blank line before BadEq class
ruff format requires two blank lines before a top-level class.
Assisted-by: Grok:4.6
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeong YunWon <jeong@youknowone.org>
* Order chmod's arguments the way CPython declares them
posix.chmod took dir_fd between path and mode. Since #8807 renders
each argument's parameter kind, that put `/` after `*`, which inspect
cannot parse, and path and mode stayed positional-only, so
os.chmod(path=..., mode=...) raised TypeError.
A ChmodArgs struct follows CPython's order, as nt.chmod already does.
path and mode are positional-or-keyword. follow_symlinks shows
CPython's text through py_default, like LinkArgs.
Assisted-by: Claude Code:claude-opus-5
* Reject a text signature that puts `/` after `*` or `**`
The writer counts positional-only parameters up front and emits `/`
when they run out, which assumes they come first in the Rust argument
list. Nothing enforced that, and posix.chmod violated it. The
signature is composed in a const context, so the assert fails the
build and names the fix.
Assisted-by: Claude Code:claude-opus-5
* Reorder the module cache atomically during imports
Add a private exact-dict move primitive and importlib dispatcher so shutdown-order updates never temporarily remove a module. Preserve current values and custom-mapping fallback behavior; validate reentrant equality probe witnesses, invalidate layout caches, and compact moved entries.
Cover all four existing bootstrap reorder sites, including legacy-loader cleanup. Add deterministic trace-window, loader replacement/removal, reentrancy, cache, iterator and compaction regressions. The private native primitive uses true-move semantics; it does not promise arbitrary pop/set callback equivalence.
Assisted-by: Codex:model-version-unavailable
* Keep atomic import regression compatible with modern bootstrap
Run legacy-loader cases when the bootstrap exposes that helper, preserving all four 3.14 paths and the two remaining 3.15 paths. Always retain modern load and exec regressions.
Assisted-by: Codex:model-version-unavailable
* Keep the copied bootstrap unchanged in the native relocation PR
Restore the exact CPython 3.14 bootstrap and retain only the private exact-dict relocation primitive and its direct native regressions. Remove the PR-added callback integration tests while preserving native helper assertions. This prepares a primitive only; imports do not call it and the import race remains unresolved.
Assisted-by: Codex:model-version-unavailable
* Match CPython's str subscript TypeError message
`str.__getitem__` with a non-index key raised the shared sequence
wording ("str indices must be integers or slices or classes that
override __index__ operator, not 'x'") instead of CPython's
str-specific "string indices must be integers, not 'x'" (bpo-44110 /
gh-88276, Python 3.11).
Add `SequenceIndex::try_from_borrowed_object_with_err`, which takes a
caller-supplied error closure, and have the existing
`try_from_borrowed_object` delegate to it with the unchanged shared
message. `str._getitem` now supplies the CPython-accurate message,
which flows through both `__getitem__` and the mapping subscript slot.
Other sequence types keep the shared wording.
Removes the expectedFailure marker on
`string_tests.StringLikeTest.test_subscript`, fixing it for both
test_str and test_userstring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fold str's subscript TypeError into the shared index helper
The previous commit added `SequenceIndex::try_from_borrowed_object_with_err`
(a closure-taking function) so `str` could raise CPython's
"string indices must be integers, not ..." message. Per review, a whole
helper API plus a thin wrapper was overkill for a single caller.
Instead, inline that one error case into `try_from_borrowed_object`: when
`type_name` is "str", raise the str-specific message; every other type keeps
the shared sequence wording. `str._getitem` goes back to the plain `match`
over `SequenceIndex` with no duplicated index parsing. A bad slice key
(e.g. `'a'[::'x']`) still surfaces the slice-specific error, since only the
non-index/non-slice branch is customized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Truncate index TypeError type names to 200 characters
Route str's non-index TypeError through a function pointer
message helper. Sequence index errors use the same 200-character
type-name cap.
Assisted-by: Grok:4.6
* Encode sequence index TypeError names as type params
`&'static str` cannot be a const generic. SequenceIndexName
holds the name as an associated constant, matching ArgName.
Both str and sequence errors go through one fn(&str) -> String
and a cold TypeError helper.
Assisted-by: Grok:4.6
---------
Co-authored-by: Jaedeok Kim <rn.6d17s2@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add the eager-runtime importlib lazy-attribute hook
Expose _imp._set_lazy_attributes(modobj, name, /), used by CPython 3.15 importlib after eager loading. Match its empty pending-lazy-module registry behavior, including name type and string-subclass hashing checks. This does not implement lazy imports or change the target version or bundled libraries.
Assisted-by: Codex:model-version-unavailable
* Consolidate lazy import hook tests into stdlib_imp
Move the distinct direct-hook argument and hashing contracts into the existing _imp snippet and remove the separate file. Preserve every pre-existing assertion and the CPython version guard; canonical 3.15 lazy-import tests already cover feature-level registry and reification behavior.
Assisted-by: Codex:model-version-unavailable
* Implement the sentinel builtin from PEP 661
Add unique sentinel values with copy/pickle identity, caller module capture, union support, and cycle collection. Compare constructor behavior and six upstream tests with CPython v3.15.0rc2; retain the existing Python target and bundled libraries.
Assisted-by: Codex:model-version-unavailable
* Remove duplicate sentinel snippet tests
Rely on the canonical Lib/test/test_builtin.py sentinel cases when the standard library is updated, as requested in PR #8945 review. Remove the custom snippet without changing the runtime implementation or existing standard-library tests.
Assisted-by: Codex:model-version-unavailable
Read the stored executable address when casting a function pointer and accept function pointer types as cast destinations. The existing ownership dictionary retains the original callback through a cast roundtrip.
Extracted independently on the unchanged Python 3.14 target, without typed callback argument or broader py_object transport changes. One unique callback-address/ownership regression extends the existing ctypes snippet; canonical coverage was checked for duplicates.
Assisted-by: Codex:model-version-unavailable
* Use populated entries for unpickler MEMOIZE indexes
A sparse BINPUT followed by MEMOIZE must use the number of occupied memo
entries, not the largest index plus one. Store populated entries in a map,
preserve ascending memo copies, reserve fallibly, and release displaced
references after memo guards end.
Keep existing memo assignment/proxy behavior and add one bounded regression
for sparse BINPUT, MEMOIZE identity and copy order. No protocol version,
GC traversal or unrelated pickle input changes are included.
Extracted from the Python 3.15 migration onto the unchanged 3.14 target.
Assisted-by: Codex:model-version-unavailable
* Move sparse memo regression to standard pickle snippet
Use the module-level snippet name requested in review without changing the regression assertions.
Assisted-by: Codex:model-version-unavailable
Return the context's canonical None singleton when standard streams are
disabled. Check the exact set type before accessing user-rebindable
conditional-annotation tracking objects.
Both fixes apply to the unchanged Python 3.14 target. Include the no-stdio
embedding regression and the unchanged canonical regression from CPython's
3.14 maintenance backport c21e7c45, after the vulnerable 3.14.7 tag.
Extracted from the larger 3.15 migration; no marshal or version changes.
Assisted-by: Codex:model-version-unavailable
* Release replaced list item outside the lock in STORE_SUBSCR_LIST_INT
Assisted-by: Claude Code:claude-opus-5-5
* Add a snippet for finalizers reading a list during a specialized store
Assisted-by: Claude Code:claude-opus-5-5
Let quote-only fields use the existing unquoted escaping path and enable
the existing CPython regression test.
Tested: test_csv, CSV snippet, 240 CPython comparisons, workspace and C-API tests, Clippy, prek
Assisted-by: Codex:gpt-6.1-sol
PyObjBag built code object constants from a Context alone, and its
frozenset arm was unimplemented!(), so ctx.new_code() panicked on code
containing a frozenset constant such as `x in {1, 2, 3}`.
IntoCodeObject now takes a VirtualMachine and uses PyVmBag for bytecode
and frozen code objects. Context::new_code, PyObjBag and
AsBag for &Context are removed; callers use VirtualMachine::new_code.
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Grok:grok-4.7
* Use AtomicPyTypeFlags in PyType_FromSlots
PyType_FromSlots still imported the old PyAtomicTypeFlags name.
Assisted-by: Grok:4.7
* Pass CPython test args as a bash array
Assisted-by: Grok:4.7
* Bump workspace version to 0.6.1
Assisted-by: Grok:4.7
* Restore the CPython test step in ci.yaml
Assisted-by: Grok:4.7
* Use bitflagset for type flags
PyTypeFlags is defined with bitflagset! and the atomic tp_flags word with
atomic_bitflagset!, replacing the hand-written PyAtomicTypeFlags.
Assisted-by: Grok:4.7
Assisted-by: Claude:claude-opus-5-5
* Use bitflagset 0.0.4 from its git main branch
bitflagset 0.0.4 is not on crates.io yet. Patch it to the git main
branch in the workspace and in the example projects that depend on
rustpython-vm, since each of them is a separate workspace.
Assisted-by: Claude:claude-opus-5-5
* Depend on crates.io bitflagset 0.0.4
bitflagset 0.0.4 is published. Drop the git patches from the workspace
and the separate example-project workspaces.
Assisted-by: Grok:4.7
* Keep empty patch tables in example workspaces
The example workspaces already have an empty [patch.crates-io] table.
Dropping the bitflagset git line leaves that table in place.
Assisted-by: Grok:4.7
* Call bitflagset methods directly for type flags
Drop the plain-set has_feature wrapper and the intersects,
is_created_with_flags, DEFAULT, and heap_type_flags aliases.
Single-bit updates use insert, and flag combinations use from_slice.
Assisted-by: Grok:4.7
* OR paired type flags in one update
HAS_WEAKREF with MANAGED_WEAKREF, and HAS_DICT with MANAGED_DICT
(and INLINE_VALUES when present), are applied with one |=.
Assisted-by: Grok:4.7
* Store heap type flag sets as constants
HEAPTYPE | BASETYPE, and the dict and immutable variants built from
it, are const values instead of from_slice calls at each use.
Assisted-by: Grok:4.7
* Test SEQUENCE and MAPPING in one load
PyTypeFlags::COLLECTION is the two-bit mask. Callers check it with
one is_disjoint instead of two contains loads.
Assisted-by: Grok:4.7
* Fix clippy 1.99 lints in the vm
Pass closure arguments by value where clippy reports a needless
borrow, drop the unfulfilled redundant_else expectation on sum, and
return the HRESULT after todo!().
Assisted-by: Grok:4.7
* Add a native _zoneinfo module
ZoneInfo reads TZif tuples from zoneinfo._common.load_data. The stdlib
module implements utcoffset, dst, tzname, fromutc, and the zone cache.
whats_left compares that class. Cache tests no longer expect failure.
Assisted-by: Grok:grok-4.7
* Fix ZoneInfo layout and keep its cache on the module
utcoffset, dst, and tzname take the object. Zone data is boxed so the
payload lines up with tzinfo on 32-bit. The strong, weak, and timedelta
caches live on each interpreter's _zoneinfo module.
Assisted-by: Grok:grok-4.7
Clippy doesn't run on everything which is why I keep accidentally
uncovering new lints to fix. I updated CI to run clippy on all features,
except tk, as well as all binaries, examples, and tests. That should
cover everything.
Tk currently doesn't build (at least for me), so that feature is still
disabled in CI.
While here, fix some of the lints that are now raised.
Some of the FFI wrappers take in a byte slice that's presumed to be NUL
capped. It's more ergonomic and safer to take in a &CStr which is
impossible to get wrong because it's validated at the margins.
Additionally, there were a few areas where a NUL was embedded directly
into the source (i.e. "COMMIT(NUL)" which displayed as "COMMIT^@"). This
had the interesting effect of breaking grep on the source code.
* zlib: clear unconsumed_tail when a bounded decompress reaches stream end
When a max_length-bounded Decompress.decompress() call reached the end
of the stream, the bytes after the stream went to unused_data, but
unconsumed_tail kept the previous call's tail, including input the call
had already consumed. A later flush() then moved those stale bytes into
unused_data.
Empty unconsumed_tail at end of stream, as CPython's
save_unconsumed_input() does since gh-158169. CPython releases before
that change report the bytes after the stream in unconsumed_tail, so
the snippet accepts either value when it runs under CPython.
Fixes#8906
Assisted-by: Claude Code:claude-opus-5-5
* zlib: bind the unconsumed input with an if expression
Choose between the empty slice and the rest of the input in one
expression instead of reassigning a mutable binding.
Assisted-by: Claude Code:claude-opus-5-5
* zlib: accept both valid tails in the snippet on every implementation
The snippet no longer branches on sys.implementation. RustPython and
CPython after gh-158169 give an empty tail; CPython before it gives the
bytes after the stream. The stale tail of gh-8906 fails either way.
Assisted-by: Claude Code:claude-opus-5-5