| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
Warning Rate limit exceeded@youknowone has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 45 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR. We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📥 CommitsReviewing files that changed from the base of the PR and between faeed2c and 60fb438. 📒 Files selected for processing (1)
WalkthroughAdds Python 3.14 bumps and implements broad PEP 649 deferred annotation support across compiler, symbol table, bytecode, VM runtime, and stdlib, plus related API, instruction, flag, exception initializer, bytes/fromhex, JIT, itertools, tests, and CI updates. Changes
Sequence Diagram(s)sequenceDiagram
participant Compiler
participant SymbolTable
participant CodeObject
participant Runtime as VM
Note over Compiler,VM: Compile-time PEP 649 annotation handling
Compiler->>SymbolTable: enter_annotation_scope()
SymbolTable->>SymbolTable: create/push annotation_block
Compiler->>Compiler: compile annotations into closure
Compiler->>CodeObject: emit annotation closure (co)
Compiler->>SymbolTable: exit_annotation_scope()
SymbolTable->>SymbolTable: pop annotation_block
Compiler->>CodeObject: attach annotations metadata (conditional map / __annotate__)
sequenceDiagram
participant Frame
participant Mapping
participant Globals
Note over Frame,Globals: LoadFromDictOrGlobals lookup (runtime)
Frame->>Frame: execute LoadFromDictOrGlobals(idx)
Frame->>Mapping: pop mapping from stack
Frame->>Mapping: try mapping lookup (dict.get / mapping protocol)
alt found
Mapping-->>Frame: push value
else not found
Frame->>Globals: load_global_or_builtin(name)
Globals-->>Frame: push value or raise NameError
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem🚥 Pre-merge checks | ✅ 4 | ❌ 1 ❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. ❤️ ShareComment @coderabbitai help to get the list of available commands and usage tips. |
Sorry, something went wrong.
|
Code has been automatically formatted The code in this PR has been formatted using:
git pull origin py-3.14 |
Sorry, something went wrong.
| @@ -39,7 +39,7 @@ | |||
| sys.exit(f"whats_left.py must be run under CPython, got {implementation} instead") | |||
| if sys.version_info[:2] < (3, 13): | |||
There was a problem hiding this comment.
| if sys.version_info[:2] < (3, 13): | |
| if sys.version_info[:2] < (3, 14): |
Sorry, something went wrong.
|
failing tests: |
Sorry, something went wrong.
|
okay! now all test passes |
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 15
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)crates/venvlauncher/src/main.rs (1)scripts/fix_test.py (1)133-133: Consider updating to Python314 for consistency.
While the test logic doesn't depend on the specific version, this test still references Python313 while test_read_home was updated to Python314. For consistency across the test suite in this version upgrade PR, consider updating this reference as well.
📝 Suggested update for consistency- writeln!(file, "home=C:\\Python313").unwrap(); + writeln!(file, "home=C:\\Python314").unwrap();And update the corresponding assertion:
- assert_eq!(home, "C:\\Python313"); + assert_eq!(home, "C:\\Python314");crates/vm/src/version.rs (1)61-67: Mutable class attribute bug: tests = [] is shared across all instances.
The tests = [] on line 63 is a class-level mutable default. All TestResult instances share the same list, so appending to test_results.tests in parse_results() mutates this shared list. If the function is called multiple times (e.g., in future refactoring), results from previous calls will persist.
🐛 Proposed fix: Initialize in `__init__`class TestResult: - tests_result: str = "" - tests = [] - stdout = "" + def __init__(self): + self.tests_result: str = "" + self.tests: list[Test] = [] + self.stdout: str = "" def __str__(self): return f"TestResult(tests_result={self.tests_result},tests={len(self.tests)})"crates/vm/src/bytes_inner.rs (1)131-132: Update PYC_MAGIC_NUMBER to 3627 for Python 3.14 compatibility.
The PYC_MAGIC_NUMBER value of 2997 is incorrect. CPython 3.14's PYC magic number is 3627. This mismatch breaks .pyc file compatibility. Update the value to:
pub const PYC_MAGIC_NUMBER: u16 = 3627;crates/codegen/src/compile.rs (2)424-469: Skip whitespace before reading the low nibble in fromhex() to match CPython behavior.
The current implementation skips ASCII whitespace before the top nibble (line 436) but not before the bottom nibble. This causes valid inputs like '1 a' or '1\na' (space/newline between nibbles of a single byte) to incorrectly raise "non-hexadecimal number found" instead of being parsed successfully. CPython's bytes.fromhex() skips all ASCII whitespace throughout the input, including between nibbles within a pair.
Test case ' 1A\n2B\t30\v' demonstrates the expected behavior—whitespace should be transparent anywhere in the hex string. The suggested fix adds a whitespace-skipping loop before the bottom nibble read, correctly mirroring the top-nibble logic.
🩹 Suggested fix- let (i, b) = match iter.next() { - Some(val) => val, - None => break None, // odd number of hex digits - }; + let (i, b) = loop { + match iter.next() { + Some((i, b)) if is_py_ascii_whitespace(b) => continue, + Some(val) => break val, + None => break None, // odd number of hex digits + } + };2132-2203: Wire in_conditional_block during conditional statement compilation.
in_conditional_block is consulted at line 5951 to determine whether annotations are conditional, but it's never toggled during conditional statement compilation in compile.rs. Annotations inside if/elif, for, while, with, and try blocks need the flag set while compiling their bodies, and it should be restored afterward.
Apply this pattern to all conditional statement paths: save the flag state, set it to true, compile the body, then restore it:
- self.compile_statements(body)?; + let prev = self.in_conditional_block; + self.in_conditional_block = true; + self.compile_statements(body)?; + self.in_conditional_block = prev;Affected locations in the Stmt::If handler (lines 2132-2203):
- Line 2147: first if body
- Line 2168: elif bodies (inside the loop)
- Line 2183: final elif/else body
Also apply to compile_while (body), compile_for (body), compile_with (body), and compile_try_statement (body).
4181-4294: Reset conditional-annotation index per class scope.
next_conditional_annotation_index is compiler-global, but compile_module_annotate enumerates class annotations from index 0 in its local scope. In nested classes, the global counter continues from where the outer class left off, causing a mismatch: the __annotate__ function will look for indices [0, 1, ...] in __conditional_annotations__, but the outer class's compiler will have added indices [N, N+1, ...], making membership checks fail.
Suggested fixfn compile_class_body( &mut self, name: &str, body: &[Stmt], type_params: Option<&TypeParams>, firstlineno: u32, ) -> CompileResult<CodeObject> { + let prev_conditional_idx = self.next_conditional_annotation_index; + self.next_conditional_annotation_index = 0; // 1. Enter class scope let key = self.symbol_table_stack.len(); self.push_symbol_table()?; @@ -4290,7 +4292,9 @@ self.emit_return_value(); // Exit scope and return the code object - Ok(self.exit_scope()) + let code = self.exit_scope(); + self.next_conditional_annotation_index = prev_conditional_idx; + Ok(code) }
In `@crates/codegen/src/compile.rs`:
- Around line 5928-5966: The conditional that only records annotation indices
when is_module || self.in_conditional_block is causing unconditional class-scope
annotations to be skipped; in the block guarded by
self.current_symbol_table().has_conditional_annotations, remove the inner if
is_conditional check and always perform the logic that takes
self.next_conditional_annotation_index (increment it), loads
__conditional_annotations__, pushes the index constant, calls
Instruction::SetAdd and Instruction::PopTop so every annotation gets its index
recorded; update references: current_symbol_table(),
has_conditional_annotations, next_conditional_annotation_index,
"__conditional_annotations__", Instruction::SetAdd and Instruction::PopTop in
compile_module_annotate (the surrounding code shown).
In `@crates/compiler-core/src/marshal.rs`:
- Line 205: FORMAT_VERSION must be incremented to reflect that CodeFlags' wire
format widened from u16 to u32; update the crate's FORMAT_VERSION constant (the
one used for serialized code objects) so previously cached/serialized data is
invalidated, ensuring deserialization in marshal.rs (where CodeFlags is read
with read_u32 in the CodeFlags::from_bits_truncate call) won't be
misinterpreted; change the numeric FORMAT_VERSION value accordingly and run
tests to confirm no other serialization paths need version bumps.
In `@crates/jit/tests/common.rs`:
- Around line 66-130: The BUILD_MAP handling in
extract_annotations_from_annotate_code only treats the map value as a name
(code.names[val_idx]) and drops const-based forward-ref annotations; update the
loop that processes each key/value pair so after extracting (_val_is_const,
val_idx) you check both possibilities: if code.names.get(val_idx) exists use
that string as now, otherwise check code.constants[val_idx] for
ConstantData::Str and use its value; insert the resulting string into
annotations as StackValue::String so forward-ref annotations like "int" are
preserved.
In `@crates/vm/src/builtins/function.rs`:
- Around line 596-679: The __annotations__ implementation currently holds the
annotations and annotate mutexes while calling the annotate callable, risking
deadlock; change it to clone the annotate callable out of self.annotate.lock(),
drop the locks, then invoke annotate_fn.call(...) without holding either lock,
then reacquire self.annotations.lock() to validate/cache the returned dict (and
handle the downcast error) before returning it; ensure set___annotate__ and
set___annotations__ behavior remains the same (they can still clear the other
lock’s cached value) so concurrent setters update state correctly.
In `@crates/vm/src/builtins/object.rs`:
- Around line 618-648: Update the instance-aware resolution: in
is_getstate_overridden and object_getstate use obj.get_attr(identifier!(vm,
__getstate__)) (not obj.class().get_attr) so instance attributes are considered;
compare the resolved attribute to object_type.get_attr(identifier!(vm,
__getstate__)) and only treat it as “not overridden” when the resolved attribute
is the exact object.__getstate__; in object_getstate, if the resolved attribute
is None return a TypeError stating __getstate__ must be callable (or handle per
CPython semantics) instead of calling it, otherwise call the resolved callable
when it differs from object.__getstate__, and fallback to
object_getstate_default only when the resolved attribute equals
object.__getstate__.
In `@crates/vm/src/builtins/template.rs`:
- Around line 177-179: The __add__ method in template.rs is missing the
#[pymethod] attribute so it won't be exported to Python; add the #[pymethod]
annotation above fn __add__(&self, other: PyObjectRef, vm: &VirtualMachine) ->
PyResult<PyRef<Self>> so the dunder addition is exposed (leaving the body
calling self.concat(&other, vm) unchanged); verify other Python dunder methods
in the same impl also have #[pymethod] if they should be exported.
- Around line 308-341: The iterator stops early because when an empty string is
skipped you set from_strings to false but do not advance index; change the logic
in PyTemplateIter::next so that in the from_strings=true branch you first check
index >= template.strings.len() and return StopIteration, then get the item and
if it is an empty PyStr increment the atomic index (zelf.index.fetch_add(1,
Ordering::SeqCst)) and continue the loop without toggling from_strings; only
flip from_strings (zelf.from_strings.store(false, ...)) when you are actually
yielding a non-empty string. This ensures skipped empty strings advance the
index and remaining strings are correctly considered even when no interpolation
exists at that position.
In `@crates/vm/src/builtins/type.rs`:
- Around line 879-905: The code in set___annotate__ stores the new
__annotate_func__ but only clears __annotations_cache__ when the new value is
not Python None, leaving stale cached annotations when __annotate__ is set to
None; change the logic in set___annotate__ (function name) to always
remove/clear the __annotations_cache__ entry (identifier!(vm,
__annotations_cache__)) after inserting the new __annotate_func__
(identifier!(vm, __annotate_func__)), i.e. unconditionally call
attrs.swap_remove(identifier!(vm, __annotations_cache__)) so the cache is
cleared whether the new value is a callable or None.
In `@crates/vm/src/frame.rs`:
- Around line 1171-1179: The current Instruction::LoadFromDictOrGlobals code
swallows all errors by calling dict.get_item(name, vm).ok(), causing
non-KeyError exceptions from custom mappings to be ignored; change the logic to
match on dict.get_item(name, vm) and: if Ok(v) push that value; if Err(e)
determine whether e is a KeyError (use the VM's KeyError sentinel, e.g.
vm.ctx.exceptions.key_error or vm.is_instance check) and only in that case fall
back to calling load_global_or_builtin(name, vm)?; for any other Err(e)
propagate the error by returning Err(e) so non-KeyError exceptions are not
swallowed (affecting the Instruction::LoadFromDictOrGlobals path that uses
pop_value, push_value, name, dict, get_item and load_global_or_builtin).
In `@crates/vm/src/stdlib/io.rs`:
- Around line 4311-4347: The __setstate__ implementation is replacing the
internal buffer even when there are active exports, violating the "no resize
while exported" rule; before mutating/replacing the buffer (where you currently
do *zelf.buffer.write() = BufferedIO::new(Cursor::new(...))), call the buffer's
try_resizable(vm)? (or explicitly check zelf.buffer.exports > 0) and return an
appropriate error if resizing is disallowed, then only perform the
BufferedIO::new(Cursor::new(...)) assignment and subsequent seek when
try_resizable succeeds; keep references to __setstate__, try_resizable, buffer,
exports, and BufferedIO::new to locate the change.
- Around line 4105-4139: The __setstate__ implementation currently replaces the
internal buffer before checking whether the StringIO is closed, allowing
mutation of a closed object; modify it to call zelf.buffer(vm)? first and check
for closed() (or otherwise validate state) and return an error if closed before
performing any mutations (i.e., creating BufferedIO::new(Cursor::new(raw_bytes))
and writing into *zelf.buffer.write()); move the buffer replacement and seek
operations to after the closed check and use the buffer(vm)? accessor for
mutations to ensure a closed StringIO cannot be mutated.
In `@extra_tests/snippets/builtin_bool.py`:
- Around line 21-23: Replace the use of set([1, 2]) with the equivalent set
literal to satisfy ruff C405; locate the set([1, 2]) occurrence in
builtin_bool.py and change it to the set literal {1, 2} so the linter warning is
resolved.
In `@extra_tests/snippets/stdlib_typing.py`:
- Line 20: Update the regression test comment string "# Regression test for:
https://github.com/RustPython/RustPython/issues/6718" to reference the correct
issue(s) this test covers (e.g., replace 6718 with 6702 or 6543 as appropriate)
or clarify that it refers to this PR if that was intended; locate the comment in
extra_tests/snippets/stdlib_typing.py (the line containing the regression test
URL) and edit the URL/text accordingly so it points to the actual issue
number(s) being closed.
In `@extra_tests/snippets/syntax_assignment.py`:
- Around line 62-73: The linter flags __annotate__ as undefined; to silence F821
without changing behavior, add a local binding that reads it from globals before
use (e.g. assign __annotate__ = globals().get("__annotate__")) and then keep the
existing checks (assert callable(__annotate__), annotations = __annotate__(1),
etc.); this resolves the static undefined-name error while preserving runtime
semantics for the symbols __annotate__ and annotations.
extra_tests/snippets/code_co_consts.py (1)📜 Review detailscrates/vm/src/stdlib/thread.rs (2)93-96: Consider adding self parameter or @staticmethod decorator to class methods.
The methods are defined without self, which works for this test since you're accessing cls_with_doc.method directly from the class to inspect __code__. However, this is unconventional and could confuse readers. Consider either:
♻️ Suggested clarification
- Adding self to make them proper instance methods, or
- Adding @staticmethod decorator to make the intent explicit
class cls_with_doc: + `@staticmethod` def method(): """Method docstring""" return 1class cls_no_doc: + `@staticmethod` def method(): return 1Also applies to: 104-106
extra_tests/snippets/stdlib_typing.py (1)901-938: Consider retaining entries when try_lock fails on inner state.
When inner.try_lock() fails at line 912-914, the entry is removed from the registry (return false). However, this thread handle still exists and may be used later. If someone calls join() on it, the thread won't be in the registry but also won't be properly marked as Done, potentially causing unexpected behavior.
Consider retaining entries when the lock cannot be acquired:
♻️ Suggested change// Try to lock the inner state - skip if we can't let Some(mut inner_guard) = inner.try_lock() else { - return false; + return true; // Keep entry - we couldn't safely process it };Alternatively, if removal is intentional, add a comment explaining that orphaned handles after fork are acceptable because the child process will create new handles for any threads it spawns.
309-321: Acknowledged: UTF-8 truncation concern.
The existing TODO comment at lines 311-312 correctly identifies that truncating at byte 15 may split a multi-byte UTF-8 character. This is a pre-existing concern and not introduced by this PR, but worth tracking.
Would you like me to open an issue to track this UTF-8 boundary handling improvement, or generate a fix that properly handles multi-byte character boundaries?
scripts/fix_test.py (1)21-21: Consider moving import to top of file.
Per PEP 8, imports should be at the top of the file. While the placement here groups the import with its related tests for clarity, moving it alongside the other typing import on line 2 would be more conventional.
Suggested refactorAt the top of the file, consolidate imports:
from collections.abc import Awaitable, Callable -from typing import TypeVar +from typing import Optional, TypeVar, UnionThen remove line 21.
crates/vm/src/builtins/int.rs (1)192-201: Redundant condition check.
The condition on line 195 (if test.result == "fail" or test.result == "error") is always true since parse_results() already filters to only include tests with these results (line 91-92). The check is harmless but could be simplified.
♻️ Optional cleanup# Collect failing tests (with deduplication for subtests) seen_tests = set() # Track (class_name, method_name) to avoid duplicates for test in tests.tests: - if test.result == "fail" or test.result == "error": - test_parts = path_to_test(test.path) - if len(test_parts) == 2: - test_key = tuple(test_parts) - if test_key not in seen_tests: - seen_tests.add(test_key) - print(f"Marking test: {test_parts[0]}.{test_parts[1]}") + test_parts = path_to_test(test.path) + if len(test_parts) == 2: + test_key = tuple(test_parts) + if test_key not in seen_tests: + seen_tests.add(test_key) + print(f"Marking test: {test_parts[0]}.{test_parts[1]}")crates/vm/src/exceptions.rs (1)290-294: Consider enabling or tracking this Python 3.14 feature.
This commented-out code for negative-to-unsigned conversion errors is part of the 3.14 upgrade. If this behavior is required for Python 3.14 compliance, consider enabling it. Otherwise, convert to a proper TODO with an issue reference to track implementation.
♻️ If intended for 3.14, enable the feature- // Python 3.14+: ValueError for negative int to unsigned type - // if I::min_value() == I::zero() && self.as_bigint().sign() == Sign::Minus { - // return Err(vm.new_value_error("Cannot convert negative int".to_owned())); - // } + // Python 3.14+: ValueError for negative int to unsigned type + if I::min_value() == I::zero() && self.as_bigint().sign() == Sign::Minus { + return Err(vm.new_value_error("cannot convert negative int to unsigned".to_owned())); + }crates/vm/src/builtins/template.rs (1)1556-1583: Minor inconsistency with other initializers.
The PyImportError initializer passes the original args (with kwargs intact) to PyBaseException::slot_init, while PyAttributeError and PyNameError explicitly create a FuncArgs with empty kwargs. Both work correctly since PyBaseException::init only uses args.args, but this is inconsistent.
Optional: Align with other initializers for consistencydict.set_item("name_from", vm.unwrap_or_none(name_from), vm)?; - PyBaseException::slot_init(zelf, args, vm) + let base_args = FuncArgs::new(args.args.clone(), KwArgs::default()); + PyBaseException::slot_init(zelf, base_args, vm)297-303: Iterator __reduce__ does not preserve iteration state.
The reducer only stores the template reference, not the current index and from_strings state. Unpickling will restart iteration from the beginning. This may be intentional (many Python iterators behave this way), but it differs from iterators that preserve position.
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
📥 CommitsReviewing files that changed from the base of the PR and between ef871d2 and f0edec0.
⛔ Files ignored due to path filters (39)📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.rs: Follow the default rustfmt code style using cargo fmt to format Rust code
Always run clippy to lint code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust
Files:
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.py: Follow PEP 8 style for custom Python code
Use ruff for linting Python code
Files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.778Z Learning: Applies to **/*.rs : Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: When comparing behavior with CPython, use `python` command to explicitly run CPython and `cargo run -- script.py` to run RustPython
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: In most cases, Python code should not be edited; bug fixes should be made through Rust code modifications only
Applied to files:
Learnt from: youknowone Repo: RustPython/RustPython PR: 6358 File: crates/vm/src/exception_group.rs:173-185 Timestamp: 2025-12-09T08:46:58.660Z Learning: In crates/vm/src/exception_group.rs, the derive() method intentionally always creates a BaseExceptionGroup instance rather than preserving the original exception class type. This is a deliberate design decision that differs from CPython's behavior.
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.778Z Learning: Applies to Lib/**/*.py : Minimize modifications to CPython standard library files in the Lib/ directory; these files should be edited very conservatively and modifications should be minimal and only to work around RustPython limitations
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.778Z Learning: Applies to Lib/**/*.py : Add a `# TODO: RUSTPYTHON` comment when modifications are made to Lib/ directory files
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: Do not edit the Lib/ directory directly except for copying files from CPython to work around RustPython limitations
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: Use `cargo run -- script.py` instead of `python script.py` when testing Python code with RustPython
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: Run a full clean build when modifying bytecode instructions using: `rm -r target/debug/build/rustpython-* && find . | grep -E "\.pyc$" | xargs rm -r`
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.779Z Learning: Run `./whats_left.py` to get a list of unimplemented methods when looking for contribution opportunities
Applied to files:
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-14T14:52:10.778Z
Learning: Applies to Lib/test/**/*.py : Use `unittest.skip("TODO: RustPython <reason>")` or `unittest.expectedFailure` with `# TODO: RUSTPYTHON <reason>` comment when marking tests in Lib/ that cannot run
Applied to files:
Learnt from: moreal Repo: RustPython/RustPython PR: 5847 File: vm/src/stdlib/stat.rs:547-567 Timestamp: 2025-06-27T14:47:28.810Z Learning: In RustPython's stat module implementation, platform-specific constants like SF_SUPPORTED and SF_SYNTHETIC should be conditionally declared only for the platforms where they're available (e.g., macOS), following CPython's approach of optional declaration using `#ifdef` checks rather than providing fallback values for other platforms.
Applied to files:
Learnt from: CR Repo: RustPython/RustPython PR: 0 File: .github/copilot-instructions.md:0-0 Timestamp: 2026-01-14T14:52:10.778Z Learning: Applies to **/test*.py : Only remove `unittest.expectedFailure` decorators and upper TODO comments from tests when tests actually pass, or add these decorators when tests cannot be fixed
Applied to files:
crates/vm/src/bytes_inner.rs (3)crates/vm/src/builtins/mod.rs (1)
- fromhex (424-470)
- string (474-474)
- fromhex_object (473-487)
crates/vm/src/stdlib/sre.rs (1)extra_tests/snippets/example_interactive.py (1)
- template (140-165)
crates/vm/src/builtins/code.rs (1)crates/vm/src/builtins/bytes.rs (1)
- co_consts (604-607)
crates/vm/src/bytes_inner.rs (3)crates/vm/src/stdlib/ast/expression.rs (1)
- fromhex (424-470)
- string (474-474)
- fromhex_object (473-487)
crates/vm/src/stdlib/ast/string.rs (1)crates/vm/src/vm/context.rs (3)
- tstring_to_object (602-618)
crates/vm/src/builtins/function.rs (2)crates/vm/src/types/zoo.rs (2)crates/vm/src/builtins/module.rs (2)
- __annotate__ (653-658)
- __annotations__ (596-626)
crates/vm/src/builtins/type.rs (2)
- __annotate__ (187-198)
- __annotations__ (224-259)
- __annotate__ (855-877)
- __annotations__ (909-949)
crates/vm/src/builtins/template.rs (1)crates/vm/src/builtins/object.rs (3)crates/vm/src/builtins/interpolation.rs (1)
- iter (234-236)
- init (205-207)
crates/vm/src/builtins/tuple.rs (4)crates/vm/src/bytes_inner.rs (1)crates/vm/src/builtins/int.rs (3)
- class (41-43)
- class (527-529)
- len (194-196)
- __getnewargs__ (352-362)
crates/vm/src/protocol/mapping.rs (1)
- __getnewargs__ (566-568)
- a (629-629)
- a (665-665)
- items (182-188)
crates/vm/src/builtins/bytes.rs (2)crates/vm/src/vm/vm_ops.rs (2)
- bytes (129-129)
- fromhex (319-323)
crates/vm/src/protocol/mapping.rs (1)extra_tests/snippets/builtin_bytearray.py (4)crates/vm/src/protocol/sequence.rs (1)
- slots (108-110)
- slots (134-136)
crates/vm/src/builtins/bytearray.rs (1)crates/vm/src/builtins/interpolation.rs (1)crates/vm/src/builtins/bytes.rs (1)
- fromhex (325-330)
crates/vm/src/bytes_inner.rs (1)
- fromhex (319-323)
extra_tests/snippets/testutils.py (1)
- fromhex (424-470)
- assert_raises (5-12)
crates/vm/src/builtins/template.rs (10)crates/compiler-core/src/bytecode/instruction.rs (1)
- class (29-31)
- class (280-282)
- new (35-40)
- new (286-292)
- py_new (46-92)
- s (149-149)
- s (154-154)
- cmp (213-230)
- other (123-123)
- init (343-346)
crates/compiler-core/src/bytecode/oparg.rs (2)crates/vm/src/frame.rs (1)
- from (25-27)
- from (68-70)
crates/vm/src/builtins/template.rs (2)extra_tests/snippets/builtins_module.py (1)
- interpolations (103-105)
- strings (98-100)
crates/vm/src/stdlib/builtins.rs (1)extra_tests/snippets/syntax_assignment.py (1)
- isinstance (527-529)
extra_tests/snippets/testutils.py (1)whats_left.py (2)
- assert_raises (5-12)
crates/vm/src/stdlib/sys.rs (2)extra_tests/snippets/builtin_bytes.py (3)crates/vm/src/stdlib/builtins.rs (1)
- version_info (1159-1161)
- implementation (495-507)
- hasattr (426-434)
crates/vm/src/builtins/bytes.rs (2)crates/vm/src/stdlib/ast/string.rs (1)crates/vm/src/builtins/bytearray.rs (1)
- bytes (129-129)
- fromhex (319-323)
crates/vm/src/bytes_inner.rs (1)
- fromhex (325-330)
- fromhex (424-470)
crates/vm/src/stdlib/ast/expression.rs (16)extra_tests/snippets/builtin_bool.py (1)
- ast_to_object (11-53)
- ast_to_object (148-165)
- ast_to_object (191-208)
- ast_to_object (234-254)
- ast_to_object (285-302)
- ast_to_object (327-344)
- ast_to_object (370-390)
- ast_to_object (421-445)
- ast_to_object (477-491)
- ast_to_object (511-528)
- ast_to_object (554-571)
- ast_to_object (597-617)
- ast_to_object (648-666)
- ast_to_object (694-708)
- ast_to_object (728-742)
- ast_to_object (761-775)
extra_tests/snippets/testutils.py (1)crates/vm/src/stdlib/io.rs (1)
- assert_raises (5-12)
crates/vm/src/builtins/object.rs (2)crates/codegen/src/compile.rs (2)
- __getstate__ (287-290)
- value (435-435)
crates/codegen/src/symboltable.rs (4)crates/vm/src/builtins/function.rs (2)crates/vm/src/builtins/function.rs (3)
- new (80-98)
- new (204-211)
- new (811-827)
- enter_annotation_scope (887-919)
- new (59-110)
- new (903-905)
- new (1025-1029)
crates/vm/src/builtins/type.rs (8)extra_tests/snippets/code_co_consts.py (1)crates/vm/src/builtins/module.rs (6)
- module (1866-1866)
- __annotations__ (909-949)
- obj (208-208)
- obj (652-652)
- obj (1192-1192)
- class (137-139)
- name (592-597)
- __annotate__ (855-877)
- new (72-77)
- __annotations__ (224-259)
- class (58-60)
- name (120-126)
- name (125-125)
- __annotate__ (187-198)
crates/vm/src/builtins/code.rs (2)🪛 Flake8 (7.3.0) extra_tests/snippets/syntax_assignment.py
- co_consts (604-607)
- co_flags (631-633)
[error] 70-70: undefined name 'annotate'
(F821)
[error] 71-71: undefined name 'annotate'
(F821)
extra_tests/snippets/code_co_consts.py[error] 87-87: do not assign a lambda expression, use a def
(E731)
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Sorry, something went wrong.
| .collect::<Result<Box<[(SourceLocation, SourceLocation)]>>>()?; | ||
|
|
||
| let flags = CodeFlags::from_bits_truncate(rdr.read_u16()?); | ||
| let flags = CodeFlags::from_bits_truncate(rdr.read_u32()?); |
There was a problem hiding this comment.
⚠️ Potential issue | 🟠 Major
🧩 Analysis chain🏁 Script executed:
fd -e rs marshal | head -20Repository: RustPython/RustPython
Length of output: 132
🏁 Script executed:
cat -n crates/compiler-core/src/marshal.rs | head -50Repository: RustPython/RustPython
Length of output: 1727
🏁 Script executed:
cat -n crates/compiler-core/src/marshal.rs | sed -n '190,220p'Repository: RustPython/RustPython
Length of output: 1461
🏁 Script executed:
cat -n crates/compiler-core/src/marshal.rs | sed -n '650,675p'Repository: RustPython/RustPython
Length of output: 1226
🏁 Script executed:
rg "FORMAT_VERSION" crates/compiler-core/src/marshal.rs -B 2 -A 2Repository: RustPython/RustPython
Length of output: 145
🏁 Script executed:
rg "CodeFlags" crates/compiler-core/src/marshal.rs -B 2 -A 2 | head -60Repository: RustPython/RustPython
Length of output: 246
🏁 Script executed:
rg "struct CodeFlags|enum CodeFlags|type CodeFlags" crates/compiler-core/src/ -B 2 -A 5Repository: RustPython/RustPython
Length of output: 619
🏁 Script executed:
rg "bitflags!" crates/compiler-core/src/marshal.rs -B 2 -A 10Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
rg -l "CodeFlags" crates/compiler-core/src/ | head -10Repository: RustPython/RustPython
Length of output: 137
🏁 Script executed:
git log --oneline crates/compiler-core/src/bytecode.rs | head -20Repository: RustPython/RustPython
Length of output: 115
🏁 Script executed:
git log --oneline crates/compiler-core/src/marshal.rs | head -20Repository: RustPython/RustPython
Length of output: 115
🏁 Script executed:
git log -p --all -- crates/compiler-core/src/bytecode.rs | grep -A 10 -B 10 "CodeFlags:" | head -50Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
git log -p --all -- crates/compiler-core/src/marshal.rs | grep -A 5 -B 5 "FORMAT_VERSION" | head -60Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cat -n crates/compiler-core/src/bytecode.rs | grep -A 20 "bitflags!"Repository: RustPython/RustPython
Length of output: 811
🏁 Script executed:
fd -i changelog -o -i "CHANGELOG" -o -i "release" | head -5Repository: RustPython/RustPython
Length of output: 181
🏁 Script executed:
git log --all --oneline | wc -lRepository: RustPython/RustPython
Length of output: 66
🏁 Script executed:
git show f0edec00e --stat | head -20Repository: RustPython/RustPython
Length of output: 1141
🏁 Script executed:
rg "cache|Cache" crates/compiler-core/src/marshal.rs -iRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
rg "compat|Compat|backwards|backward" crates/compiler-core/src/marshal.rs -iRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
git log --all --oneline -- "*/marshal.rs" | head -30Repository: RustPython/RustPython
Length of output: 167
🏁 Script executed:
git show HEAD:crates/compiler-core/src/bytecode.rs | grep -A 15 "bitflags!"Repository: RustPython/RustPython
Length of output: 580
🏁 Script executed:
git show 380fa39eb --stat | grep -E "(marshal|bytecode)"Repository: RustPython/RustPython
Length of output: 654
🏁 Script executed:
git show 380fa39eb -- crates/compiler-core/src/bytecode.rs | grep -A 20 -B 5 "CodeFlags"Repository: RustPython/RustPython
Length of output: 2802
🏁 Script executed:
git show 380fa39eb -- crates/compiler-core/src/marshal.rs | head -100Repository: RustPython/RustPython
Length of output: 2675
🏁 Script executed:
git log --all --oneline --follow -- "crates/compiler-core/src/bytecode.rs" | head -20Repository: RustPython/RustPython
Length of output: 167
🏁 Script executed:
git show 380fa39eb:crates/compiler-core/src/bytecode.rs | grep -A 10 "pub struct CodeFlags"Repository: RustPython/RustPython
Length of output: 332
🏁 Script executed:
git diff 380fa39eb HEAD -- crates/compiler-core/src/bytecode.rs | grep -A 10 -B 10 "CodeFlags"Repository: RustPython/RustPython
Length of output: 885
🏁 Script executed:
git log --all --oneline --follow -- "crates/compiler-core/src/bytecode.rs" | grep -v "Auto-format"Repository: RustPython/RustPython
Length of output: 116
🏁 Script executed:
git log --all --reverse --oneline | head -20Repository: RustPython/RustPython
Length of output: 167
🏁 Script executed:
git show HEAD:crates/compiler-core/src/bytecode.rs | grep "pub const FORMAT_VERSION"Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
git show 380fa39eb:crates/compiler-core/src/bytecode.rs | grep "pub const FORMAT_VERSION"Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
git log --all --oneline -p -- crates/compiler-core/src/bytecode.rs crates/compiler-core/src/marshal.rs | grep -A 2 -B 2 "FORMAT_VERSION" | head -30Repository: RustPython/RustPython
Length of output: 239
🏁 Script executed:
git show 380fa39eb:crates/compiler-core/src/marshal.rs | sed -n '200,210p'Repository: RustPython/RustPython
Length of output: 382
🏁 Script executed:
git show 380fa39eb:crates/compiler-core/src/bytecode.rs | grep -A 8 "pub struct CodeFlags"Repository: RustPython/RustPython
Length of output: 308
🏁 Script executed:
rg "FORMAT_VERSION" crates/ -B 2 -A 2Repository: RustPython/RustPython
Length of output: 581
🏁 Script executed:
git show 380fa39eb:crates/compiler-core/src/marshal.rs | sed -n '660,670p'Repository: RustPython/RustPython
Length of output: 356
🏁 Script executed:
rg "read_u16|write_u16" crates/compiler-core/src/marshal.rsRepository: RustPython/RustPython
Length of output: 146
🏁 Script executed:
rg "test_marshal|test.*code.*object" --type rust | head -20Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
rg "pub fn dump|pub fn load" crates/compiler-core/src/marshal.rsRepository: RustPython/RustPython
Length of output: 47
Bump FORMAT_VERSION: CodeFlags wire format changed from u16 to u32.
CodeFlags was widened from u16 to u32 between commits, but FORMAT_VERSION remains at 4. This creates a backward compatibility issue: any pre-existing serialized code objects will misalign on deserialization. Increment FORMAT_VERSION to invalidate stale cached data.
🤖 Prompt for AI AgentsIn `@crates/compiler-core/src/marshal.rs` at line 205, FORMAT_VERSION must be incremented to reflect that CodeFlags' wire format widened from u16 to u32; update the crate's FORMAT_VERSION constant (the one used for serialized code objects) so previously cached/serialized data is invalidated, ensuring deserialization in marshal.rs (where CodeFlags is read with read_u32 in the CodeFlags::from_bits_truncate call) won't be misinterpreted; change the numeric FORMAT_VERSION value accordingly and run tests to confirm no other serialization paths need version bumps.
Sorry, something went wrong.
| /// Extract annotations from an annotate function's bytecode. | ||
| /// The annotate function uses BUILD_MAP with key-value pairs loaded before it. | ||
| /// Keys are parameter names (from LOAD_CONST), values are type names (from LOAD_NAME/LOAD_GLOBAL). | ||
| fn extract_annotations_from_annotate_code(code: &CodeObject) -> HashMap<String, StackValue> { | ||
| let mut annotations = HashMap::new(); | ||
| let mut stack: Vec<(bool, usize)> = Vec::new(); // (is_const, index) | ||
| let mut op_arg_state = OpArgState::default(); | ||
|
|
||
| for &word in code.instructions.iter() { | ||
| let (instruction, arg) = op_arg_state.get(word); | ||
|
|
||
| match instruction { | ||
| Instruction::LoadConst { idx } => { | ||
| stack.push((true, idx.get(arg) as usize)); | ||
| } | ||
| Instruction::LoadName(idx) | Instruction::LoadGlobal(idx) => { | ||
| stack.push((false, idx.get(arg) as usize)); | ||
| } | ||
| Instruction::BuildMap { size, .. } => { | ||
| let count = size.get(arg) as usize; | ||
| // Stack has key-value pairs in order: k1, v1, k2, v2, ... | ||
| // So we need count * 2 items from the stack | ||
| let start = stack.len().saturating_sub(count * 2); | ||
| let pairs: Vec<_> = stack.drain(start..).collect(); | ||
|
|
||
| for chunk in pairs.chunks(2) { | ||
| if chunk.len() == 2 { | ||
| let (key_is_const, key_idx) = chunk[0]; | ||
| let (_val_is_const, val_idx) = chunk[1]; | ||
|
|
||
| // Key should be a const string (parameter name) | ||
| if key_is_const | ||
| && let ConstantData::Str { value } = &code.constants[key_idx] | ||
| { | ||
| let param_name = value.to_string_lossy().into_owned(); | ||
| // Value should be a name (type name) | ||
| if let Some(type_name) = code.names.get(val_idx) { | ||
| annotations | ||
| .insert(param_name, StackValue::String(type_name.clone())); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Return after processing BUILD_MAP - we got our annotations | ||
| return annotations; | ||
| } | ||
| Instruction::Resume { .. } | ||
| | Instruction::LoadFast(_) | ||
| | Instruction::CompareOp { .. } | ||
| | Instruction::ExtendedArg => { | ||
| // Ignore these instructions for annotation extraction | ||
| } | ||
| Instruction::ReturnValue | Instruction::ReturnConst { .. } => { | ||
| // End of function - return what we have | ||
| return annotations; | ||
| } | ||
| _ => { | ||
| // For other instructions, clear the stack tracking as we don't understand the effect | ||
| stack.clear(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| annotations | ||
| } |
There was a problem hiding this comment.
⚠️ Potential issue | 🟡 Minor
🧩 Analysis chain🏁 Script executed:
cd crates/jit && find . -name "common.rs" -type fRepository: RustPython/RustPython
Length of output: 82
🏁 Script executed:
cd crates/jit && cat -n tests/common.rs | head -150 | tail -100Repository: RustPython/RustPython
Length of output: 4784
🏁 Script executed:
cd crates/jit && grep -n "StackValue" tests/common.rs | head -20Repository: RustPython/RustPython
Length of output: 1448
🏁 Script executed:
cd crates/jit && grep -n "annotations" tests/common.rs | head -30Repository: RustPython/RustPython
Length of output: 1302
🏁 Script executed:
cd crates/jit && grep -B5 -A10 "extract_annotations_from_annotate_code" tests/common.rs | head -40Repository: RustPython/RustPython
Length of output: 1727
🏁 Script executed:
cd crates/jit && grep -r "annotations" tests/ --include="*.py" | head -20Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cd crates/jit && grep -B10 -A10 "def.*->.*\"" tests/ --include="*.py" 2>/dev/null | head -40Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cd crates/jit && find tests -name "*.py" -type f | head -10Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cd crates/jit && grep -A20 "def .*annotate" tests/common.rsRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cd crates/jit && python3 << 'EOF'
# Verify that forward refs in Python annotations are LOAD_CONST
import dis
import io
code_str = '''
def annotate():
return {
'x': "int",
'y': str
}
'''
exec(code_str)
print("Bytecode for forward ref ('int' string) vs reference (str type):")
print("=" * 60)
dis.dis(annotate)
EOFRepository: RustPython/RustPython
Length of output: 464
🏁 Script executed:
cd crates/jit && grep -r "def.*:.*\"" tests/common.rsRepository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
cd crates/jit && sed -n '200,250p' tests/common.rsRepository: RustPython/RustPython
Length of output: 2762
🏁 Script executed:
cd crates/jit && grep -B5 -A15 "SetFunctionAttribute" tests/common.rsRepository: RustPython/RustPython
Length of output: 1218
🏁 Script executed:
cd crates/jit && python3 << 'EOF'
# Verify StackValue can hold strings from both const and names
import json
# Test case 1: Forward ref annotation (LOAD_CONST string)
test_forward_ref = """
def annotate():
return {
'param': "ForwardRef",
}
"""
# Test case 2: Type reference annotation (LOAD_NAME/LOAD_GLOBAL)
test_type_ref = """
def annotate():
return {
'param': str,
}
"""
print("Test 1 - Forward ref (string literal):")
print(test_forward_ref)
print("\nTest 2 - Type reference (name):")
print(test_type_ref)
print("\nBoth should work with proper annotation extraction.")
EOFRepository: RustPython/RustPython
Length of output: 316
Handle string-literal annotation values too.
Forward-ref style annotations like "int" are LOAD_CONST values; the current code only checks code.names for values, silently dropping any const-based annotations. This causes forward refs to be ignored, resulting in missing annotations.
🛠️ Suggested fix- let (_val_is_const, val_idx) = chunk[1];
+ let (val_is_const, val_idx) = chunk[1];
// Key should be a const string (parameter name)
if key_is_const
&& let ConstantData::Str { value } = &code.constants[key_idx]
{
let param_name = value.to_string_lossy().into_owned();
- // Value should be a name (type name)
- if let Some(type_name) = code.names.get(val_idx) {
- annotations
- .insert(param_name, StackValue::String(type_name.clone()));
- }
+ // Value can be a name or a const string (forward ref)
+ let type_name = if val_is_const {
+ match &code.constants[val_idx] {
+ ConstantData::Str { value } => {
+ Some(value.to_string_lossy().into_owned())
+ }
+ _ => None,
+ }
+ } else {
+ code.names.get(val_idx).cloned()
+ };
+ if let Some(type_name) = type_name {
+ annotations.insert(param_name, StackValue::String(type_name));
+ }In `@crates/jit/tests/common.rs` around lines 66 - 130, The BUILD_MAP handling in extract_annotations_from_annotate_code only treats the map value as a name (code.names[val_idx]) and drops const-based forward-ref annotations; update the loop that processes each key/value pair so after extracting (_val_is_const, val_idx) you check both possibilities: if code.names.get(val_idx) exists use that string as now, otherwise check code.constants[val_idx] for ConstantData::Str and use its value; insert the resulting string into annotations as StackValue::String so forward-ref annotations like "int" are preserved.
Sorry, something went wrong.
| #[pymethod] | ||
| fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> { | ||
| if state.len() != 4 { | ||
| return Err(vm.new_type_error(format!( | ||
| "__setstate__ argument should be 4-tuple, got {}", | ||
| state.len() | ||
| ))); | ||
| } | ||
|
|
||
| let content: PyStrRef = state[0].clone().try_into_value(vm)?; | ||
| // state[1] is newline - TODO: use when newline handling is implemented | ||
| let pos: u64 = state[2].clone().try_into_value(vm)?; | ||
| let dict = &state[3]; | ||
|
|
||
| // Set content | ||
| let raw_bytes = content.as_bytes().to_vec(); | ||
| *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); | ||
|
|
||
| // Set position | ||
| zelf.buffer(vm)? | ||
| .seek(SeekFrom::Start(pos)) | ||
| .map_err(|err| os_err(vm, err))?; | ||
|
|
||
| // Set __dict__ if provided | ||
| if !vm.is_none(dict) { | ||
| let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?; | ||
| if let Some(obj_dict) = zelf.as_object().dict() { | ||
| obj_dict.clear(); | ||
| for (key, value) in dict_ref.into_iter() { | ||
| obj_dict.set_item(&*key, value, vm)?; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(()) |
There was a problem hiding this comment.
⚠️ Potential issue | 🟡 Minor
Avoid mutating closed StringIO during setstate.
__setstate__ replaces the buffer before any closed check, so a closed object can be mutated even though an error is later raised. Fail fast (or use buffer(vm)? for the mutation) to keep state consistent.
@@
#[pymethod]
fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
+ if zelf.closed.load() {
+ return Err(io_closed_error(vm));
+ }
if state.len() != 4 {
return Err(vm.new_type_error(format!(
"__setstate__ argument should be 4-tuple, got {}",
state.len()
)));
}
@@
let dict = &state[3];
// Set content
let raw_bytes = content.as_bytes().to_vec();
- *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes));
+ let mut buffer = zelf.buffer(vm)?;
+ *buffer = BufferedIO::new(Cursor::new(raw_bytes));
// Set position
- zelf.buffer(vm)?
- .seek(SeekFrom::Start(pos))
+ buffer
+ .seek(SeekFrom::Start(pos))
.map_err(|err| os_err(vm, err))?;
+ drop(buffer);
@@
Ok(())
}‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[pymethod] | |
| fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> { | |
| if state.len() != 4 { | |
| return Err(vm.new_type_error(format!( | |
| "__setstate__ argument should be 4-tuple, got {}", | |
| state.len() | |
| ))); | |
| } | |
| let content: PyStrRef = state[0].clone().try_into_value(vm)?; | |
| // state[1] is newline - TODO: use when newline handling is implemented | |
| let pos: u64 = state[2].clone().try_into_value(vm)?; | |
| let dict = &state[3]; | |
| // Set content | |
| let raw_bytes = content.as_bytes().to_vec(); | |
| *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); | |
| // Set position | |
| zelf.buffer(vm)? | |
| .seek(SeekFrom::Start(pos)) | |
| .map_err(|err| os_err(vm, err))?; | |
| // Set __dict__ if provided | |
| if !vm.is_none(dict) { | |
| let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?; | |
| if let Some(obj_dict) = zelf.as_object().dict() { | |
| obj_dict.clear(); | |
| for (key, value) in dict_ref.into_iter() { | |
| obj_dict.set_item(&*key, value, vm)?; | |
| } | |
| } | |
| } | |
| Ok(()) | |
| #[pymethod] | |
| fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> { | |
| if zelf.closed.load() { | |
| return Err(io_closed_error(vm)); | |
| } | |
| if state.len() != 4 { | |
| return Err(vm.new_type_error(format!( | |
| "__setstate__ argument should be 4-tuple, got {}", | |
| state.len() | |
| ))); | |
| } | |
| let content: PyStrRef = state[0].clone().try_into_value(vm)?; | |
| // state[1] is newline - TODO: use when newline handling is implemented | |
| let pos: u64 = state[2].clone().try_into_value(vm)?; | |
| let dict = &state[3]; | |
| // Set content | |
| let raw_bytes = content.as_bytes().to_vec(); | |
| let mut buffer = zelf.buffer(vm)?; | |
| *buffer = BufferedIO::new(Cursor::new(raw_bytes)); | |
| // Set position | |
| buffer | |
| .seek(SeekFrom::Start(pos)) | |
| .map_err(|err| os_err(vm, err))?; | |
| drop(buffer); | |
| // Set __dict__ if provided | |
| if !vm.is_none(dict) { | |
| let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?; | |
| if let Some(obj_dict) = zelf.as_object().dict() { | |
| obj_dict.clear(); | |
| for (key, value) in dict_ref.into_iter() { | |
| obj_dict.set_item(&*key, value, vm)?; | |
| } | |
| } | |
| } | |
| Ok(()) | |
| } |
In `@crates/vm/src/stdlib/io.rs` around lines 4105 - 4139, The __setstate__ implementation currently replaces the internal buffer before checking whether the StringIO is closed, allowing mutation of a closed object; modify it to call zelf.buffer(vm)? first and check for closed() (or otherwise validate state) and return an error if closed before performing any mutations (i.e., creating BufferedIO::new(Cursor::new(raw_bytes)) and writing into *zelf.buffer.write()); move the buffer replacement and seek operations to after the closed check and use the buffer(vm)? accessor for mutations to ensure a closed StringIO cannot be mutated.
Sorry, something went wrong.
| #[pymethod] | ||
| fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> { | ||
| if zelf.closed.load() { | ||
| return Err(vm.new_value_error("__setstate__ on closed file")); | ||
| } | ||
| if state.len() != 3 { | ||
| return Err(vm.new_type_error(format!( | ||
| "__setstate__ argument should be 3-tuple, got {}", | ||
| state.len() | ||
| ))); | ||
| } | ||
|
|
||
| let content: PyBytesRef = state[0].clone().try_into_value(vm)?; | ||
| let pos: u64 = state[1].clone().try_into_value(vm)?; | ||
| let dict = &state[2]; | ||
|
|
||
| // Set content | ||
| *zelf.buffer.write() = BufferedIO::new(Cursor::new(content.as_bytes().to_vec())); | ||
|
|
||
| // Set position | ||
| zelf.buffer(vm)? | ||
| .seek(SeekFrom::Start(pos)) | ||
| .map_err(|err| os_err(vm, err))?; | ||
|
|
||
| // Set __dict__ if provided | ||
| if !vm.is_none(dict) { | ||
| let dict_ref: PyRef<PyDict> = dict.clone().try_into_value(vm)?; | ||
| if let Some(obj_dict) = zelf.as_object().dict() { | ||
| obj_dict.clear(); | ||
| for (key, value) in dict_ref.into_iter() { | ||
| obj_dict.set_item(&*key, value, vm)?; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
⚠️ Potential issue | 🟠 Major
Respect active exports in BytesIO.__setstate__ to avoid invalidating memoryviews.
__setstate__ replaces the buffer even when exports > 0, which violates the existing “no resize while exported” rule. Use try_resizable(vm)? (or check exports) before mutating the buffer.
@@
#[pymethod]
fn __setstate__(zelf: PyRef<Self>, state: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
if zelf.closed.load() {
return Err(vm.new_value_error("__setstate__ on closed file"));
}
+ if zelf.exports.load() > 0 {
+ return Err(vm.new_buffer_error(
+ "Existing exports of data: object cannot be re-sized".to_owned(),
+ ));
+ }
if state.len() != 3 {
return Err(vm.new_type_error(format!(
"__setstate__ argument should be 3-tuple, got {}",
state.len()
)));
}
@@
let dict = &state[2];
// Set content
- *zelf.buffer.write() = BufferedIO::new(Cursor::new(content.as_bytes().to_vec()));
+ let mut buffer = zelf.try_resizable(vm)?;
+ *buffer = BufferedIO::new(Cursor::new(content.as_bytes().to_vec()));
// Set position
- zelf.buffer(vm)?
- .seek(SeekFrom::Start(pos))
+ buffer
+ .seek(SeekFrom::Start(pos))
.map_err(|err| os_err(vm, err))?;
+ drop(buffer);
@@
Ok(())
}In `@crates/vm/src/stdlib/io.rs` around lines 4311 - 4347, The __setstate__ implementation is replacing the internal buffer even when there are active exports, violating the "no resize while exported" rule; before mutating/replacing the buffer (where you currently do *zelf.buffer.write() = BufferedIO::new(Cursor::new(...))), call the buffer's try_resizable(vm)? (or explicitly check zelf.buffer.exports > 0) and return an appropriate error if resizing is disallowed, then only perform the BufferedIO::new(Cursor::new(...)) assignment and subsequent seek when try_resizable succeeds; keep references to __setstate__, try_resizable, buffer, exports, and BufferedIO::new to locate the change.
Sorry, something went wrong.
| # Test that Union/Optional in function parameter annotations work correctly. | ||
| # This tests that annotation scopes can access global implicit symbols (like Union) | ||
| # that are imported at module level but not explicitly bound in the function scope. | ||
| # Regression test for: https://github.com/RustPython/RustPython/issues/6718 |
There was a problem hiding this comment.
⚠️ Potential issue | 🟡 Minor
Verify the issue reference.
The comment references #6718, but according to the PR objectives, that is this PR's number, not an issue. The issues being closed are #6702 and #6543. Consider updating the reference to point to the actual issue this regression test covers.
🤖 Prompt for AI AgentsIn `@extra_tests/snippets/stdlib_typing.py` at line 20, Update the regression test comment string "# Regression test for: https://github.com/RustPython/RustPython/issues/6718" to reference the correct issue(s) this test covers (e.g., replace 6718 with 6702 or 6543 as appropriate) or clarify that it refers to this PR if that was intended; locate the comment in extra_tests/snippets/stdlib_typing.py (the line containing the regression test URL) and edit the URL/text accordingly so it points to the actual issue number(s) being closed.
Sorry, something went wrong.
| @@ -73,7 +73,7 @@ def parse_results(result): | |||
| test_results.stdout = result.stdout | |||
| in_test_results = False | |||
| for line in lines: | |||
| if line == "Run tests sequentially": | |||
| if "Run" in line and "sequentially" in line: | |||
There was a problem hiding this comment.
Would it make more sense for this to be a regular expression match?
Sorry, something went wrong.
There was a problem hiding this comment.
that makes sense. This is merged in #6748 . I will create a separated PR for it
Oh! confused
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)crates/vm/src/bytes_inner.rs (1)crates/codegen/src/compile.rs (1)424-469: Fix whitespace handling to match CPython 3.14 behavior.
The implementation only skips whitespace before the high nibble, but CPython's bytes.fromhex() ignores ASCII whitespace anywhere in the input. Input like "0 1" would currently raise "non-hexadecimal number found in fromhex() arg at position X" instead of successfully parsing it as b"\x01".
Add whitespace skipping before parsing the low nibble (after fetching the second byte in the pair) to match CPython's behavior.
4240-4293: Reset next_conditional_annotation_index for each class scope.
Without resetting per class, the annotation index drifts from previous module or class compilation, causing the __annotate__ function to reference incorrect indices. Save and restore the counter around class body compilation.
Proposed fix@@ - // 2. Set up class namespace + // 2. Set up class namespace + // Reset conditional-annotation index for class scope (restore after) + let saved_conditional_index = self.next_conditional_annotation_index; + self.next_conditional_annotation_index = 0; @@ - // Exit scope and return the code object - Ok(self.exit_scope()) + // Exit scope and return the code object + let code = self.exit_scope(); + self.next_conditional_annotation_index = saved_conditional_index; + Ok(code)
In `@crates/stdlib/src/ssl/error.rs`:
- Around line 7-11: Remove the unused import types::Constructor from the use
statement in this file; the #[pyexception(...)] macros here do not use
Constructor (or Initializer), so delete Constructor from the import list to
silence the clippy unused-import warning and keep only the actually used symbols
(e.g., Py, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::{PyBaseException, PyOSError, PyStrRef}).
In `@extra_tests/snippets/stdlib_socket.py`:
- Around line 134-136: Remove the commented-out assertion for socket.htonl(-1)
and replace it with a proper test: either (A) fix the Rust implementation so
socket.htonl raises OverflowError for out-of-range values and then
restore/uncomment the test asserting the correct exception on socket.htonl(-1),
or (B) keep the test but mark it with unittest.skip or unittest.expectedFailure
(and a short TODO referencing the tracking issue) so the test suite does not
contain commented code; target the test function that contains the
socket.htonl(-1) assertion and update its decorator accordingly.
crates/compiler-core/src/marshal.rs (1)🧹 Nitpick comments (5)crates/jit/tests/common.rs (1)205-205: Bump marshal FORMAT_VERSION for 32‑bit CodeFlags.
✅ Suggested change
CodeFlags is now serialized as u32, but the format version is still 4, so older cached data will deserialize incorrectly. Please increment the format version to invalidate stale marshaled objects.-pub const FORMAT_VERSION: u32 = 4; +pub const FORMAT_VERSION: u32 = 5;extra_tests/snippets/syntax_assignment.py (1)91-108: Forward-ref string annotations are silently dropped.
The _val_is_const flag is ignored at line 94, so when annotations use forward-reference strings like "int" (which compile to LOAD_CONST), the value lookup at line 102 will fail since it only checks code.names, not code.constants. This causes forward-ref annotations to be silently omitted.
Suggested fix- let (_val_is_const, val_idx) = chunk[1]; + let (val_is_const, val_idx) = chunk[1]; // Key should be a const string (parameter name) if key_is_const && let ConstantData::Str { value } = &code.constants[key_idx] { let param_name = value.to_string_lossy().into_owned(); - // Value should be a name (type name) - if let Some(type_name) = code.names.get(val_idx) { - annotations - .insert(param_name, StackValue::String(type_name.clone())); - } + // Value can be a name (type ref) or a const string (forward ref) + let type_name = if val_is_const { + match &code.constants.get(val_idx) { + Some(ConstantData::Str { value }) => { + Some(value.to_string_lossy().into_owned()) + } + _ => None, + } + } else { + code.names.get(val_idx).cloned() + }; + if let Some(type_name) = type_name { + annotations.insert(param_name, StackValue::String(type_name)); + } }crates/vm/src/builtins/type.rs (1)62-73: Silence F821 lint warnings on __annotate__ usage.
Static analysis flags __annotate__ as undefined on lines 70-71, but this is a runtime-injected name per PEP 649. Consider adding # noqa: F821 comments to keep lint clean if these snippets are linted in CI.
Suggested lint-safe tweak-assert callable(__annotate__) -annotations = __annotate__(1) # 1 = FORMAT_VALUE +assert callable(__annotate__) # noqa: F821 +annotations = __annotate__(1) # noqa: F821 # 1 = FORMAT_VALUEcrates/codegen/src/compile.rs (1)958-988: Verify __annotations__ assignment behavior against CPython 3.14.
Per PEP 649/749, assigning to cls.__annotations__ in CPython 3.14 stores directly to __annotations__ in the class dict and sets __annotate__ to None. The current implementation conditionally updates __annotations__ vs __annotations_cache__ based on whether __annotations__ already exists, which may diverge from CPython's behavior where assignment always writes to __annotations__.
CPython 3.14 PEP 649 class __annotations__ assignment behavior5928-5966: Always record executed simple annotations when conditional tracking is enabled.
🐛 Proposed fix
Right now indices are only recorded for module scope or when in_conditional_block is true, which drops unconditional class annotations when has_conditional_annotations is set. Record every executed simple annotation to keep indices aligned with compile_module_annotate.- let is_module = self.current_symbol_table().typ == CompilerScope::Module; - let is_conditional = is_module || self.in_conditional_block; - - if is_conditional { - // Get the current annotation index and increment - let annotation_index = self.next_conditional_annotation_index; - self.next_conditional_annotation_index += 1; - - // Add index to __conditional_annotations__ set - let cond_annotations_name = self.name("__conditional_annotations__"); - emit!(self, Instruction::LoadName(cond_annotations_name)); - self.emit_load_const(ConstantData::Integer { - value: annotation_index.into(), - }); - emit!(self, Instruction::SetAdd { i: 0_u32 }); - emit!(self, Instruction::PopTop); - } + // Record every executed simple annotation when conditional handling is enabled + let annotation_index = self.next_conditional_annotation_index; + self.next_conditional_annotation_index += 1; + + let cond_annotations_name = self.name("__conditional_annotations__"); + emit!(self, Instruction::LoadName(cond_annotations_name)); + self.emit_load_const(ConstantData::Integer { + value: annotation_index.into(), + }); + emit!(self, Instruction::SetAdd { i: 0_u32 }); + emit!(self, Instruction::PopTop);
scripts/auto_mark_test.py (1)extra_tests/snippets/code_co_consts.py (1)67-74: Consider using __init__ instead of mutable class attributes.
tests and unexpected_successes are mutable class attributes shared by all instances. While parse_results() mitigates this by reassigning new lists, this pattern can cause subtle bugs if usage changes.
♻️ Suggested refactorclass TestResult: - tests_result: str = "" - tests = [] - unexpected_successes = [] # Tests that passed but were marked as expectedFailure - stdout = "" + def __init__(self): + self.tests_result: str = "" + self.tests: list[Test] = [] + self.unexpected_successes: list[Test] = [] # Tests that passed but were marked as expectedFailure + self.stdout: str = ""crates/codegen/src/symboltable.rs (3)24-25: Consider defining CO_HAS_DOCSTRING as a named constant.
The magic number 0x4000000 is used repeatedly throughout the file. Defining it as a constant at the top would improve readability and maintainability.
Suggested improvement+# CO_HAS_DOCSTRING flag added in Python 3.14 +CO_HAS_DOCSTRING = 0x4000000 + # Test function with docstring - docstring should be co_consts[0] def with_doc(): """This is a docstring""" return 1 assert with_doc.__code__.co_consts[0] == "This is a docstring", ( with_doc.__code__.co_consts ) assert with_doc.__doc__ == "This is a docstring" -# Check CO_HAS_DOCSTRING flag (0x4000000) -assert with_doc.__code__.co_flags & 0x4000000, hex(with_doc.__code__.co_flags) +# Check CO_HAS_DOCSTRING flag +assert with_doc.__code__.co_flags & CO_HAS_DOCSTRING, hex(with_doc.__code__.co_flags)383-412: Consider avoiding clone of class symbols for annotation scope.
The symbols.clone() at line 389 may be expensive for large class scopes. This clone is needed to avoid borrow conflicts, but consider whether passing a reference with appropriate lifetime management could work instead.
However, given the complexity of the borrow checker constraints with the with_append pattern and the fact that annotation blocks are relatively rare, this is acceptable for correctness.
932-956: Consider consolidating the helper methods.
Both add_classdict_freevar and add_conditional_annotations_freevar follow identical patterns with only the name differing. Consider a single parameterized helper.
♻️ Optional refactor to reduce duplication- fn add_classdict_freevar(&mut self) { - let table = self.tables.last_mut().unwrap(); - let name = "__classdict__"; - let symbol = table - .symbols - .entry(name.to_owned()) - .or_insert_with(|| Symbol::new(name)); - symbol.scope = SymbolScope::Free; - symbol - .flags - .insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS); - } - - fn add_conditional_annotations_freevar(&mut self) { - let table = self.tables.last_mut().unwrap(); - let name = "__conditional_annotations__"; - let symbol = table - .symbols - .entry(name.to_owned()) - .or_insert_with(|| Symbol::new(name)); - symbol.scope = SymbolScope::Free; - symbol - .flags - .insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS); - } + fn add_class_freevar(&mut self, name: &str) { + let table = self.tables.last_mut().unwrap(); + let symbol = table + .symbols + .entry(name.to_owned()) + .or_insert_with(|| Symbol::new(name)); + symbol.scope = SymbolScope::Free; + symbol + .flags + .insert(SymbolFlags::REFERENCED | SymbolFlags::FREE_CLASS); + }Then call as self.add_class_freevar("__classdict__") and self.add_class_freevar("__conditional_annotations__").
1048-1055: Clarify the rationale for module-only re-scan.
The comment explains that class scope should not re-scan to preserve class-local symbol resolution, but the rationale for why module scope needs re-scanning could be clearer. Consider adding a comment explaining that module-level annotations may reference global builtins (like str, int) that need to be registered in the module scope's symbol table for proper code generation.
Sorry, something went wrong.
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agentsIn `@crates/codegen/src/symboltable.rs`:
- Around line 1093-1137: The parent scope's annotation_block is only saved for
Module/Class, causing parent annotation_block to be taken (and lost) when the
parent is a Function; change the logic to always save the parent's
annotation_block before calling enter_scope_with_parameters/scan_statements and
restore it afterward. Concretely, replace the matches-based
should_save_annotation_block and conditional take with an unconditional let
saved_annotation_block =
self.tables.last_mut().unwrap().annotation_block.take(); then after leave_scope
(and after optional type param leave) restore it with if let Some(block) =
saved_annotation_block { self.tables.last_mut().unwrap().annotation_block =
Some(block); }, keeping existing calls to enter_type_param_block,
scan_type_params, enter_scope_with_parameters, scan_statements, and leave_scope
intact.
- Around line 2165-2176: The guard that prevents assignment to "__debug__"
currently checks role against SymbolUsage::Parameter,
SymbolUsage::AnnotationParameter, and SymbolUsage::Assigned but omits
SymbolUsage::AnnotationAssigned, so annotated assignments like `__debug__: int =
1` are allowed; update the match in the block that returns SymbolTableError (the
check comparing name == "__debug__" and matches!(role, ...)) to include
SymbolUsage::AnnotationAssigned alongside the other variants so annotated
assignments produce the same "cannot assign to __debug__" error using the
existing SymbolTableError and location variables.
- Around line 885-930: enter_annotation_scope currently clears
self.current_varnames and leave_annotation_scope overwrites table.varnames,
which drops the pre-seeded "format" and the enclosing function's varnames; fix
by saving/restoring the parent's current_varnames around annotation scopes and
seeding current_varnames from the annotation block's existing varnames: in
enter_annotation_scope, before taking current.annotation_block, save let
parent_varnames = core::mem::take(&mut self.current_varnames), then when you
push the annotation table set self.current_varnames = table.varnames.clone() (or
merge so "format" is preserved); in leave_annotation_scope, before storing
table.varnames = core::mem::take(&mut self.current_varnames) restore the saved
parent_varnames back into self.current_varnames so enclosing scope names are not
lost; keep the "format" seed in the annotation_table.varnames when creating it
and ensure add_classdict_freevar/add_conditional_annotations_freevar logic (and
future_annotations checks) still runs while the annotation table is active.
In `@extra_tests/snippets/code_co_consts.py`:
- Around line 99-101: The test for cls_with_doc.method is missing an assertion
that the CO_HAS_DOCSTRING flag is set; update the test to assert that
cls_with_doc.method.__code__.co_flags & CO_HAS_DOCSTRING != 0 (using the
CO_HAS_DOCSTRING symbol) in addition to the existing checks of co_consts[0] and
__doc__ so it matches the other "with_doc" test cases.
crates/vm/src/frame.rs (1)🧹 Nitpick comments (4)crates/jit/tests/common.rs (1)1171-1193: LGTM! Past review concern properly addressed.
The implementation now correctly handles errors from custom mappings:
- For PyDict: uses get_item_opt with ? to propagate errors while returning None for missing keys
- For non-dict mappings: explicitly matches on the error type, only treating KeyError as "not found" and propagating other exceptions
This resolves the previous concern about swallowing non-KeyError exceptions when using .ok().
extra_tests/snippets/syntax_assignment.py (1)91-105: Handle const-string annotation values in annotate extraction.
Line 94 ignores val_is_const, so forward-ref annotations compiled as LOAD_CONST "int" are dropped (or can mis-map by index into code.names). This can yield missing annotations and JIT panics for annotated args.
🛠️ Suggested fix- let (_val_is_const, val_idx) = chunk[1]; + let (val_is_const, val_idx) = chunk[1]; @@ - // Value should be a name (type name) - if let Some(type_name) = code.names.get(val_idx) { - annotations - .insert(param_name, StackValue::String(type_name.clone())); - } + // Value can be a name or a const string (forward ref) + let type_name = if val_is_const { + match &code.constants[val_idx] { + ConstantData::Str { value } => { + Some(value.to_string_lossy().into_owned()) + } + _ => None, + } + } else { + code.names.get(val_idx).cloned() + }; + if let Some(type_name) = type_name { + annotations.insert(param_name, StackValue::String(type_name)); + }crates/codegen/src/compile.rs (1)70-71: Add # noqa: F821 to silence static analysis warnings for __annotate__.
The static analyzer flags __annotate__ as undefined because it's a runtime-injected name per PEP 649. This will cause linting failures in CI.
crates/compiler-core/src/marshal.rs (1)5928-5966: Unconditional class annotations are still skipped in conditional tracking.
When has_conditional_annotations is true, indices are only recorded for is_module || in_conditional_block (Line 5950-5953). In class scope, unconditional annotations at the top level won’t be recorded, so __annotate__ will filter them out. This matches a previously reported issue.
🐛 Suggested fix (record every executed simple annotation)- let is_module = self.current_symbol_table().typ == CompilerScope::Module; - let is_conditional = is_module || self.in_conditional_block; - - if is_conditional { - // Get the current annotation index and increment - let annotation_index = self.next_conditional_annotation_index; - self.next_conditional_annotation_index += 1; - - // Add index to __conditional_annotations__ set - let cond_annotations_name = self.name("__conditional_annotations__"); - emit!(self, Instruction::LoadName(cond_annotations_name)); - self.emit_load_const(ConstantData::Integer { - value: annotation_index.into(), - }); - emit!(self, Instruction::SetAdd { i: 0_u32 }); - emit!(self, Instruction::PopTop); - } + // Record every executed simple annotation when conditional handling is enabled. + let annotation_index = self.next_conditional_annotation_index; + self.next_conditional_annotation_index += 1; + + let cond_annotations_name = self.name("__conditional_annotations__"); + emit!(self, Instruction::LoadName(cond_annotations_name)); + self.emit_load_const(ConstantData::Integer { + value: annotation_index.into(), + }); + emit!(self, Instruction::SetAdd { i: 0_u32 }); + emit!(self, Instruction::PopTop);crates/vm/src/builtins/type.rs (2)7-7: Bump marshal FORMAT_VERSION for widened CodeFlags.
CodeFlags are now serialized as u32, but FORMAT_VERSION remains 4. This will misread older caches. Please bump the format version to invalidate stale data.
🛠️ Proposed fix-pub const FORMAT_VERSION: u32 = 4; +pub const FORMAT_VERSION: u32 = 5;Also applies to: 205-205, 663-663
879-904: Cache clearing fix applied correctly
The previous review comment suggested unconditionally clearing __annotations_cache__ when __annotate__ is updated. Line 901 now performs attrs.swap_remove(identifier!(vm, __annotations_cache__)) unconditionally after setting the new value, which addresses the stale cache concern.
958-990: Verify alignment with CPython 3.14 __annotations__ assignment semantics
The conditional logic writes to either __annotations__ or __annotations_cache__ depending on whether __annotations__ already exists. Per the previous review comment, CPython 3.14 (PEP 649) always stores assigned values directly in __annotations__ within the class __dict__, not conditionally to a cache.
Consider simplifying to always write to __annotations__ on assignment:
🩹 Suggested fixfn set___annotations__(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) -> PyResult<()> { if self.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE) { return Err(vm.new_type_error(format!( "cannot set '__annotations__' attribute of immutable type '{}'", self.name() ))); } let mut attrs = self.attributes.write(); - // conditional update based on __annotations__ presence - let has_annotations = attrs.contains_key(identifier!(vm, __annotations__)); - - if has_annotations { - // If __annotations__ is in dict, update it - if let Some(value) = value { - attrs.insert(identifier!(vm, __annotations__), value); - } else if attrs - .swap_remove(identifier!(vm, __annotations__)) - .is_none() - { - return Err(vm.new_attribute_error("__annotations__".to_owned())); - } - // Also clear __annotations_cache__ - attrs.swap_remove(identifier!(vm, __annotations_cache__)); - } else { - // Otherwise update only __annotations_cache__ - if let Some(value) = value { - attrs.insert(identifier!(vm, __annotations_cache__), value); - } else if attrs - .swap_remove(identifier!(vm, __annotations_cache__)) - .is_none() - { - return Err(vm.new_attribute_error("__annotations__".to_owned())); - } - } + if let Some(value) = value { + attrs.insert(identifier!(vm, __annotations__), value); + } else if attrs.swap_remove(identifier!(vm, __annotations__)).is_none() + && attrs.swap_remove(identifier!(vm, __annotations_cache__)).is_none() + { + return Err(vm.new_attribute_error("__annotations__".to_owned())); + } + // Clear cache on any update + attrs.swap_remove(identifier!(vm, __annotations_cache__)); // Always clear __annotate_func__ and __annotate__ attrs.swap_remove(identifier!(vm, __annotate_func__)); attrs.swap_remove(identifier!(vm, __annotate__)); Ok(()) }CPython 3.14 PEP 649 class __annotations__ setter behavior
extra_tests/snippets/code_co_consts.py (1)crates/vm/src/exception_group.rs (1)1-34: Consider extracting the magic number to a named constant.
The CO_HAS_DOCSTRING flag value 0x4000000 is used multiple times throughout the file. Extracting it to a named constant at the top would improve readability and maintainability.
♻️ Suggested improvement+# CO_HAS_DOCSTRING flag introduced in Python 3.14 +CO_HAS_DOCSTRING = 0x4000000 + # Test function with docstring - docstring should be co_consts[0] def with_doc():Then replace all occurrences of 0x4000000 with CO_HAS_DOCSTRING.
crates/vm/src/builtins/module.rs (2)351-364: The kwargs validation logic is correct, but Line 362 is redundant.
The explicit kwargs rejection aligns with CPython's BaseExceptionGroup.__init__ behavior, and the decision not to call PyBaseException::slot_init is correctly documented—slot_new already initializes args to (message, exceptions_tuple).
However, Line 362 (let _ = (zelf, args, vm);) is unnecessary since all three variables are already used: zelf.class().name() in the error message, args.kwargs in the check, and vm for error creation.
🔧 Suggested cleanupreturn Err(vm.new_type_error(format!( "{} does not take keyword arguments", zelf.class().name() ))); } // Do NOT call PyBaseException::slot_init here. // slot_new already set args to (message, exceptions_tuple). // Calling base init would overwrite with original args (message, exceptions_list). - let _ = (zelf, args, vm); Ok(())200-221: Consider clearing __annotations__ unconditionally when setting __annotate__
The current logic only clears __annotations__ when the new value is not None (line 214). However, per PEP 649 semantics, setting __annotate__ to None should also invalidate any cached annotations since the annotation source is being explicitly removed.
This mirrors the issue previously flagged in type.rs. Consider unconditionally clearing __annotations__:
🩹 Suggested fixPySetterValue::Assign(value) => { if !vm.is_none(&value) && !value.is_callable() { return Err(vm.new_type_error("__annotate__ must be callable or None")); } let dict = zelf.dict(); dict.set_item(identifier!(vm, __annotate__), value.clone(), vm)?; - // Clear __annotations__ if value is not None - if !vm.is_none(&value) { - dict.del_item(identifier!(vm, __annotations__), vm).ok(); - } + // Clear cached __annotations__ on any change to __annotate__ + dict.del_item(identifier!(vm, __annotations__), vm).ok(); Ok(()) }
261-269: Improve robustness of is_initializing helper
The current implementation may panic or behave unexpectedly if _initializing attribute access fails for reasons other than missing attribute. Consider handling the error case more explicitly:
🩹 Suggested fixfn is_initializing(dict: &PyDictRef, vm: &VirtualMachine) -> bool { if let Ok(Some(spec)) = dict.get_item_opt(vm.ctx.intern_str("__spec__"), vm) - && let Ok(initializing) = spec.get_attr(vm.ctx.intern_str("_initializing"), vm) { - return initializing.try_to_bool(vm).unwrap_or(false); + if let Ok(initializing) = spec.get_attr(vm.ctx.intern_str("_initializing"), vm) { + return initializing.try_to_bool(vm).unwrap_or(false); + } } false }The suggested fix is equivalent but may be clearer for future readers. The current code is functionally correct.
Sorry, something went wrong.
| // Save the parent's annotation_block before scanning function annotations, | ||
| // so function annotations don't interfere with parent scope annotations. | ||
| // This applies to both class scope (methods) and module scope (top-level functions). | ||
| let parent_scope_typ = self.tables.last().map(|t| t.typ); | ||
| let should_save_annotation_block = matches!( | ||
| parent_scope_typ, | ||
| Some(CompilerScope::Class) | Some(CompilerScope::Module) | ||
| ); | ||
| let saved_annotation_block = if should_save_annotation_block { | ||
| self.tables.last_mut().unwrap().annotation_block.take() | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| // For generic functions, enter type_param block FIRST so that | ||
| // annotation scopes are nested inside and can see type parameters. | ||
| if let Some(type_params) = type_params { | ||
| self.enter_type_param_block( | ||
| &format!("<generic parameters of {}>", name.as_str()), | ||
| self.line_index_start(type_params.range), | ||
| )?; | ||
| self.scan_type_params(type_params)?; | ||
| } | ||
| let has_return_annotation = if let Some(expression) = returns { | ||
| self.scan_annotation(expression)?; | ||
| true | ||
| } else { | ||
| false | ||
| }; | ||
| self.enter_scope_with_parameters( | ||
| name.as_str(), | ||
| parameters, | ||
| self.line_index_start(*range), | ||
| has_return_annotation, | ||
| )?; | ||
| self.scan_statements(body)?; | ||
| self.leave_scope(); | ||
| if type_params.is_some() { | ||
| self.leave_scope(); | ||
| } | ||
|
|
||
| // Restore parent's annotation_block after processing the function | ||
| if let Some(block) = saved_annotation_block { | ||
| self.tables.last_mut().unwrap().annotation_block = Some(block); | ||
| } |
There was a problem hiding this comment.
⚠️ Potential issue | 🔴 Critical
🧩 Analysis chain🏁 Script executed:
# First, let's understand the CompilerScope enum and its definition
cd crates/codegen/src && rg -n "enum CompilerScope" -A 10 symboltable.rsRepository: RustPython/RustPython
Length of output: 312
🏁 Script executed:
# Let's look for how annotation_block is used across different scopes
cd crates/codegen/src && rg -n "annotation_block" symboltable.rs | head -30Repository: RustPython/RustPython
Length of output: 1914
🏁 Script executed:
# Get the full function context around lines 1093-1137
cd crates/codegen/src && sed -n '1050,1150p' symboltable.rsRepository: RustPython/RustPython
Length of output: 4277
🏁 Script executed:
cd crates/codegen/src && rg -n "fn enter_scope_with_parameters" -A 30 symboltable.rsRepository: RustPython/RustPython
Length of output: 1245
🏁 Script executed:
# Also check where annotation_block is set/taken for functions
cd crates/codegen/src && sed -n '2100,2150p' symboltable.rsRepository: RustPython/RustPython
Length of output: 2016
Unconditionally save/restore parent annotation_block to avoid clobbering in nested functions.
The save/restore guard only covers Module and Class parent scopes. When a function with annotation_block contains a nested function with annotations, the parent's block is taken by enter_scope_with_parameters (line 2123), losing the parent's annotations. This occurs specifically when a Function scope is the parent (not just Class/Module), which the current scope-type check misses.
🛠️ Proposed fix- let parent_scope_typ = self.tables.last().map(|t| t.typ);
- let should_save_annotation_block = matches!(
- parent_scope_typ,
- Some(CompilerScope::Class) | Some(CompilerScope::Module)
- );
- let saved_annotation_block = if should_save_annotation_block {
- self.tables.last_mut().unwrap().annotation_block.take()
- } else {
- None
- };
+ let saved_annotation_block =
+ self.tables.last_mut().and_then(|t| t.annotation_block.take());In `@crates/codegen/src/symboltable.rs` around lines 1093 - 1137, The parent
scope's annotation_block is only saved for Module/Class, causing parent
annotation_block to be taken (and lost) when the parent is a Function; change
the logic to always save the parent's annotation_block before calling
enter_scope_with_parameters/scan_statements and restore it afterward.
Concretely, replace the matches-based should_save_annotation_block and
conditional take with an unconditional let saved_annotation_block =
self.tables.last_mut().unwrap().annotation_block.take(); then after leave_scope
(and after optional type param leave) restore it with if let Some(block) =
saved_annotation_block { self.tables.last_mut().unwrap().annotation_block =
Some(block); }, keeping existing calls to enter_type_param_block,
scan_type_params, enter_scope_with_parameters, scan_statements, and leave_scope
intact.
Sorry, something went wrong.
| // Check for forbidden names like __debug__ | ||
| if name == "__debug__" | ||
| && matches!( | ||
| role, | ||
| SymbolUsage::Parameter | SymbolUsage::AnnotationParameter | SymbolUsage::Assigned | ||
| ) | ||
| { | ||
| return Err(SymbolTableError { | ||
| error: "cannot assign to __debug__".to_owned(), | ||
| location, | ||
| }); | ||
| } |
There was a problem hiding this comment.
⚠️ Potential issue | 🟠 Major
🧩 Analysis chain🏁 Script executed:
# First, let's look at the file and understand the context around the code
wc -l crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 103
🏁 Script executed:
# Read the code around lines 2165-2176 with context
sed -n '2150,2190p' crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 1588
🏁 Script executed:
# Find the SymbolUsage enum definition to understand all variants
rg "enum SymbolUsage" -A 15 crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 337
🏁 Script executed:
# Search for AnnotationAssigned variant in the codebase
rg "AnnotationAssigned" crates/codegen/src/Repository: RustPython/RustPython
Length of output: 338
🏁 Script executed:
# Search for other __debug__ checks to understand the pattern
rg "__debug__" crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 211
🏁 Script executed:
# Find where AnnotationAssigned is registered to understand the context
rg "AnnotationAssigned" -B 3 -A 3 crates/codegen/src/symboltable.rs | head -40Repository: RustPython/RustPython
Length of output: 983
🏁 Script executed:
# Look for tests related to __debug__ validation
find crates -name "*.rs" -type f | xargs rg "__debug__" -lRepository: RustPython/RustPython
Length of output: 161
🏁 Script executed:
# Check if there are test files for symboltable
fd "test" crates/codegen/src/ -type fRepository: RustPython/RustPython
Length of output: 235
🏁 Script executed:
# Search for __debug__ in test files
rg "__debug__" crates/compiler/test_fixtures/ -l 2>/dev/null | head -10Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Check the compile.rs file to see how __debug__ is handled there
rg "__debug__" -B 3 -A 3 crates/codegen/src/compile.rsRepository: RustPython/RustPython
Length of output: 1068
🏁 Script executed:
# Let's also verify the context where AnnotationAssigned is used
sed -n '2000,2050p' crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 1911
🏁 Script executed:
# Check the full pattern matching logic for SymbolUsage to understand what should be covered
rg "SymbolUsage::" crates/codegen/src/symboltable.rs | grep -E "(Parameter|Assigned|Annotation)" | head -20Repository: RustPython/RustPython
Length of output: 1550
🏁 Script executed:
# Let's look at the specific line where AnnotationAssigned is registered to confirm the context
sed -n '1750,1800p' crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 2542
🏁 Script executed:
# Also check what the error message context is and verify this guard is about preventing assignment
sed -n '2160,2200p' crates/codegen/src/symboltable.rsRepository: RustPython/RustPython
Length of output: 1734
🏁 Script executed:
# Search for how Python handles __debug__ to confirm the semantics
rg "class.*annotation" crates/compiler/test_fixtures/ -i --max-count=5Repository: RustPython/RustPython
Length of output: 135
🏁 Script executed:
# Look for test files related to symboltable or __debug__
find crates -name "*.py" -o -name "*.rs" | xargs grep -l "test.*__debug__" 2>/dev/null | head -5Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Let's also check if there's documentation or examples of annotated assignments
rg "AnnotationAssigned" -B 5 -A 10 crates/codegen/src/symboltable.rs | head -50Repository: RustPython/RustPython
Length of output: 2137
🏁 Script executed:
# Verify the proposed diff is syntactically valid Rust
cat > /tmp/test_syntax.rs << 'EOF'
enum SymbolUsage {
Parameter,
AnnotationParameter,
Assigned,
AnnotationAssigned,
}
fn test() {
let role = SymbolUsage::Assigned;
// Current version
let _check1 = matches!(
role,
SymbolUsage::Parameter | SymbolUsage::AnnotationParameter | SymbolUsage::Assigned
);
// Proposed version
let _check2 = matches!(
role,
SymbolUsage::Parameter
| SymbolUsage::AnnotationParameter
| SymbolUsage::Assigned
| SymbolUsage::AnnotationAssigned
);
}
EOF
rustc --crate-type lib /tmp/test_syntax.rs 2>&1 | head -20Repository: RustPython/RustPython
Length of output: 109
__debug__ assignment guard misses annotated assignments.
SymbolUsage::AnnotationAssigned isn't covered, so __debug__: int = 1 would slip through. This should remain a syntax error.
🛠️ Proposed fix- if name == "__debug__"
- && matches!(
- role,
- SymbolUsage::Parameter | SymbolUsage::AnnotationParameter | SymbolUsage::Assigned
- )
+ if name == "__debug__"
+ && matches!(
+ role,
+ SymbolUsage::Parameter
+ | SymbolUsage::AnnotationParameter
+ | SymbolUsage::Assigned
+ | SymbolUsage::AnnotationAssigned
+ )
{
return Err(SymbolTableError {
error: "cannot assign to __debug__".to_owned(),
location,
});
}‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Check for forbidden names like __debug__ | |
| if name == "__debug__" | |
| && matches!( | |
| role, | |
| SymbolUsage::Parameter | SymbolUsage::AnnotationParameter | SymbolUsage::Assigned | |
| ) | |
| { | |
| return Err(SymbolTableError { | |
| error: "cannot assign to __debug__".to_owned(), | |
| location, | |
| }); | |
| } | |
| // Check for forbidden names like __debug__ | |
| if name == "__debug__" | |
| && matches!( | |
| role, | |
| SymbolUsage::Parameter | |
| | SymbolUsage::AnnotationParameter | |
| | SymbolUsage::Assigned | |
| | SymbolUsage::AnnotationAssigned | |
| ) | |
| { | |
| return Err(SymbolTableError { | |
| error: "cannot assign to __debug__".to_owned(), | |
| location, | |
| }); | |
| } |
In `@crates/codegen/src/symboltable.rs` around lines 2165 - 2176, The guard that prevents assignment to "__debug__" currently checks role against SymbolUsage::Parameter, SymbolUsage::AnnotationParameter, and SymbolUsage::Assigned but omits SymbolUsage::AnnotationAssigned, so annotated assignments like `__debug__: int = 1` are allowed; update the match in the block that returns SymbolTableError (the check comparing name == "__debug__" and matches!(role, ...)) to include SymbolUsage::AnnotationAssigned alongside the other variants so annotated assignments produce the same "cannot assign to __debug__" error using the existing SymbolTableError and location variables.
Sorry, something went wrong.
| assert cls_with_doc.method.__code__.co_consts[0] == "Method docstring" | ||
| assert cls_with_doc.method.__doc__ == "Method docstring" | ||
|
|
There was a problem hiding this comment.
⚠️ Potential issue | 🟡 Minor
Missing CO_HAS_DOCSTRING flag assertion for consistency.
All other "with_doc" test cases verify the CO_HAS_DOCSTRING flag is set, but this class method test omits that check. Add the flag assertion for complete coverage.
🔧 Suggested fix assert cls_with_doc.method.__code__.co_consts[0] == "Method docstring"
assert cls_with_doc.method.__doc__ == "Method docstring"
+assert cls_with_doc.method.__code__.co_flags & 0x4000000In `@extra_tests/snippets/code_co_consts.py` around lines 99 - 101, The test for cls_with_doc.method is missing an assertion that the CO_HAS_DOCSTRING flag is set; update the test to assert that cls_with_doc.method.__code__.co_flags & CO_HAS_DOCSTRING != 0 (using the CO_HAS_DOCSTRING symbol) in addition to the existing checks of co_consts[0] and __doc__ so it matches the other "with_doc" test cases.
Sorry, something went wrong.
|
Still have issues to fix, but merge this one as it is to encourage 3.14 update. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
close #6702
close #6543
Python 3.14 appears to be a release with a significant number of changes.
This PR currently includes PEP 649 and PEP 750, along with various other behavior changes and deprecations.
Summary by CodeRabbit
New Features
Bug Fixes
Behavior Changes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.