* Update `test_unicodedata.py` to 3.14.4
* Add test asset
* Mark failing tests
* Add `expectedSuccess` docorator
* Add support for nee decorator in patch_spec
* Reapply patches
The uppercase/titlecase branch of PyStr::title() pushed characters
unchanged when starting a new word, which left Latin Extended-B
digraphs (U+01F1 'DZ', U+01C4 'DŽ', etc.) in their uppercase form
instead of mapping them to their distinct titlecase counterparts
(U+01F2 'Dz', U+01C5 'Dž'). For ASCII letters and characters where
to_titlecase is identity this had no effect, hiding the bug for the
common case.
Mirror the lowercase branch — which already calls to_titlecase()
when starting a new word — so both branches symmetrically apply
the titlecase mapping. char::to_titlecase is identity for already-
titlecase and ASCII-uppercase characters, so existing cases stay
correct.
Also unmasks test_unicodedata.UnicodeMiscTest.test_bug_4971, which
asserts exactly this behavior (`'DŽ'.title() == 'Dž'` etc.)
and was marked expectedFailure with reason `+ Dž`.
Closes#7527 (the only example from that issue still failing on
3.14.4; the other four examples already pass on current main).
float_ops::divmod, mod_, and floordiv each carried their own conversion
from Rust's dividend-sign `%` to CPython's divisor-sign convention. Both
divmod and mod_ mishandled the zero-remainder case where the dividend
is a non-zero exact multiple of the divisor (e.g. divmod(6.0, -3.0),
6.0 % -3.0): the sign-correction branch fired on a zero remainder and
produced (-3.0, -3.0) and -3.0 respectively, violating the magnitude
invariant 0 <= abs(r) < abs(b). divmod also leaked the wrong signed-
zero quotient when the true quotient was zero (divmod(-1.0, -2.0)
returned (-0.0, -1.0) instead of (+0.0, -1.0)).
These are independent bugs in two functions, but both come from the
same root cause: zero-remainder needs a separate path from the sign-
correction branch.
Mirror CPython's `_float_div_mod` (Objects/floatobject.c) by making
divmod the canonical implementation and turning mod_ and floordiv into
thin wrappers. divmod(a, b) == (a // b, a % b) now holds by
construction.
Closes#7722
PR #7727 added an overflow guard in float_pow's negative-base
non-integer-exponent branch (which delegates to complex_pow). The
remaining else branch — covering positive base or negative-base
integer exponent — directly returned v1.powf(v2) without inspecting
the result, so finite inputs that produced an out-of-range value
silently leaked f64::INFINITY (or -INFINITY) instead of raising.
Examples that now raise OverflowError as in CPython:
pow(2.0, 2000)
pow(10.0, 400)
pow(-2.0, 2001)
pow(0.5, -2000)
pow(1e150, 3)
Mirror the inline overflow guard already used in complex.rs::complex_pow:
when the result is infinite but neither input is, raise OverflowError.
The both-finite check preserves intentional infinities like
pow(inf, 2.0).
Closes#7729
CPython splits PathLike resolution across two layers: _io.FileIO
preserves the raw argument on .name (low-level contract), while
the high-level open() wrapper resolves it via os.fspath before
delegating. RustPython's io_open skipped that step, so
builtins.open(PathLike).name returned the PathLike object instead
of the resolved str/bytes.
Match Lib/_pyio.py:193-194 by resolving non-int inputs at the
io_open boundary, leaving _io.FileIO unchanged.
* typealias reviews
* Bytecode parity - try/except block order, CFG reorder
Reorder try/except/else/finally to emit else+finally before
except handlers matching CPython layout. Add set_no_location
for cleanup blocks. Extend CFG reorder pass to handle true-path
jump-back for generators, break/continue, and assert in loops.
Add stop-iteration error handler awareness to block protection.
* Align CFG cleanup bytecode with CPython
* Unmark test_dis.test_findlabels expected failure
* Compute target predecessor flags in single pass
remove_nops and remove_redundant_nops_in_blocks repeated
has_jump_predecessor / has_plain_jump_predecessor / target lookups
per block, scanning all blocks each time. With ~200,000 if blocks
this became O(B^2 * I) and timed out test_compile.test_stack_overflow.
Fold the three flags into one O(B * I) pass via
compute_target_predecessor_flags.
* Address review feedback on slice folding, fallthrough, attr-chain
- try_fold_constant_slice now delegates to try_fold_constant_expr so
slice bounds accept the same constants other folding paths do
(unary-folded values, __debug__, etc.).
- remove_nops resolves fallthrough predecessors through empty blocks
via next_nonempty_block before checking ends_with_for_cleanup.
- should_deopt_borrowed_attr_chain ReturnIter matcher now accepts
Instruction::CallKw alongside Instruction::Call, matching the
Call/CallKw treatment in the surrounding deopt trigger check.
* remove test
* Fix del obj.__dict__ to match CPython behavior (issue #5355)
* Address CodeRabbit concerns: fix GC clearing and improve thread safety of lazy __dict__ recreation
* Fix del obj.__dict__: improve GC safety, implement lazy re-creation in setattr, and enable passing CPython tests
* Restore expectedFailure for test_has_inline_values
* Fix ObjExt::new call site to include has_dict parameter
* Remove stray test.py to avoid CI syntax errors
* Remove debug txt files and clean test_class.py comments
* Delete Lib/test/test_class.py
* Restore test_class.py with correct changes (remove expectedFailure, no deletion)
* Fix clippy warnings: remove unused into_inner, collapse nested if-let
* Fix rustfmt formatting and ruff PEP 8 E302 blank line
* Align __dict__ error messages and ensure safety for function/partial objects
* Fix compilation errors: change &self to &Py<Self> in __dict__ methods
* Fix compilation errors: resolve borrow-after-move and replace transpose on PySetterValue
- type.rs: Replace invalid .transpose() on PySetterValue with explicit
match on Assign/Delete variants in subtype_set_dict
- function.rs: Fix borrow-after-move in set___dict__ by capturing class
name before downcast; use as_object() for instance_dict/set_dict calls
- _functools.rs: Same borrow-after-move fix and as_object() calls for
PyPartial's __dict__ getter/setter
* Fix compilation errors: resolve borrow-after-move and replace transpose on PySetterValue
* Fix snippet formatting and mark test_remote as expected failure
* Fix test_remote by removing HAS_DICT flag from function type
* Fix lint formatting error
* Remove unnecessary print statement in test_del_dict
* Fix trailing newlines in snippet test
* Trigger CI
* Align __dict__ generic setter behavior
* Move __dict__ deletion tests to relevant snippets
---------
Co-authored-by: Jeong, YunWon <jeong@youknowone.org>
* Short-circuit identity in rich_compare_bool for Eq/Ne (PyObject_RichCompareBool parity)
CPython distinguishes two comparison entry points (Objects/object.c):
- PyObject_RichCompare returns the raw __eq__ / __ne__ result; no
identity short-circuit
- PyObject_RichCompareBool returns bool; identity implies equality
(and inequality is false on identity), short-circuiting before
dispatch
Collection membership / equality (x in [x], [nan] == [nan], set/dict
comparisons) go through the bool variant and rely on the short-circuit.
RustPython's rich_compare_bool skipped the identity check, so a buggy
or raising __eq__ propagated even when the operand was the same object.
Add an identity short-circuit at the top of rich_compare_bool for Eq
(returns true) and Ne (returns false). Ordering ops fall through to
_cmp because Python does not guarantee reflexivity for </<=/>/>=.
_cmp itself is untouched, so == / != operators continue to invoke
__eq__ / __ne__ exactly as before.
Unmasks test_dictviews.TestDictViews.test_compare_error.
Verified byte-identical with CPython 3.14.4 across 53 scenarios in 10
categories (collection membership / equality / ordering ops / NaN /
hash collision / dict views / list-set-dict ops). 14-module regression
sweep ~2,402 tests passes with no regressions.
* Route proxy comparisons through PyObject_RichCompare
The identity short-circuit added to rich_compare_bool exposed a latent
bug in three proxy types (weakref, weakproxy, mappingproxy): they were
delegating their __eq__ to the bool variant on referents, while CPython
uses PyObject_RichCompare so the referent's __eq__ runs even when the
referents share identity.
Fixes test_weak_keyed_cascading_deletes which depends on key __eq__
firing during dict deletion to trigger a side-effect that mutates the
key list.
PEP 479 wraps a StopIteration raised from a generator/coroutine into a
RuntimeError. CPython sets both __cause__ and __context__ of the new
RuntimeError to the original StopIteration (the same object). RustPython's
wrapping sites set __cause__ only, leaving __context__ as None.
set___cause__ already toggles __suppress_context__ to true, so the
user-facing traceback is unchanged — only assertions that inspect
__context__ directly observe the gap.
Three sites all wrap StopIteration / StopAsyncIteration into RuntimeError
for PEP 479. Set __context__ to the same exception instance before
setting __cause__ at each:
- frame.rs::StopIterationError intrinsic (generator yield-expression path)
- coroutine.rs generator __next__ StopIteration branch
- coroutine.rs async-generator __anext__ StopAsyncIteration branch
PyBaseExceptionRef is Arc-backed, so e.clone() is a refcount bump;
'cause is context' holds, matching CPython.
Unmasks:
- test_generator_stop.TestPEP479.test_stopiteration_wrapping_context
- test_yield_from.TestInterestingEdgeCases.test_close_and_throw_work
- test_yield_from.TestInterestingEdgeCases.test_close_and_throw_raise_stop_iteration
- test_yield_from.TestInterestingEdgeCases.test_close_and_throw_yield
CPython's PySequence_Contains (Objects/abstract.c::_PySequence_IterSearch)
catches the TypeError from PyObject_GetIter when neither __contains__ nor
__iter__ is available, and re-raises with membership-test wording.
RustPython's PySequence::contains propagated the get_iter error verbatim,
so '1 in obj' produced 'X' object is not iterable — a literal but less
specific message.
Wrap the get_iter call in map_err: when the failure is a TypeError,
re-raise with byte-identical CPython wording. Other exception types pass
through unchanged.
Unmasks test_contains.TestContains.test_common_tests.
CPython's compile() (Python/Python-ast.c) accepts any object with
__index__ for the flags and optimize arguments. RustPython's CompileArgs
typed both fields as OptionalArg<PyIntRef>, so a class with only
__index__ raised 'TypeError: Expected type int but X found' during arg
binding.
bpo-36907's regression test (test_call.test_fastcall_clearing_dict)
exercises exactly this: an IntWithDict.__index__ that mutates
self.kwargs mid-call. CPython parses both args via __index__ and doesn't
crash even when the kwargs dict is cleared between binding and use.
Switch flags and optimize to OptionalArg<ArgPrimitiveIndex<i32>>, the
same helper already used by range, slice, bytes.__mul__, hex, oct, etc.
ArgPrimitiveIndex calls try_index (= __index__ protocol) and converts
to the requested primitive in one step, so the three call sites in
compile() simplify from .map_or(Ok(d), |v| v.try_to_primitive(vm))? to
.map_or(d, |v| v.value).
Unmasks test_call.FastCallTests.test_fastcall_clearing_dict.
* Make mappingproxy's nb_or slot symmetric for dict | mp
CPython's mappingproxy_or (Objects/descrobject.c) unwraps a mappingproxy
on either side of '|' to its underlying mapping and delegates back to
PyNumber_Or. This makes 'dict | mp', 'mp | dict', and 'mp | mp' all
produce a plain dict result without dict's own nb_or having to know
about mappingproxy.
RustPython's 'or' slot in 'AsNumber for PyMappingProxy' only handled
the case where the left operand is a mappingproxy. When the slot fired
with a=dict, b=mp (because dict.nb_or returned NotImplemented), it
returned NotImplemented again — so 'dict | mp' raised TypeError. This
propagated to 'UserDict | mappingproxy' (which calls self.data | other).
Rewrite the slot to unwrap a mappingproxy on either side (or both)
before delegating to vm._or on the underlying mappings. The
inplace_or slot is unchanged — '|=' is still rejected.
Unmasks test_userdict.UserDictTest.test_mixed_or.
* Unmask test_types.MappingProxyTests.test_union
The slot fix in this PR also enables this test, which was marked
expectedFailure for the same dict|mappingproxy TypeError. Caught as
UNEXPECTED SUCCESS in CI.
CPython's defaultdict.__missing__ (Modules/_collectionsmodule.c::defdict_missing)
calls default_factory() first; if the factory's recursion already populated
self[key] while running, the existing value is preserved instead of being
overwritten.
RustPython ships a Python fallback at Lib/collections/_defaultdict.py
(the C _collections.defaultdict is not available). That fallback
unconditionally executed self[key] = val after the factory returned,
overwriting any value the recursive call had already stored.
Add a 'if key in self: return self[key]' guard before the assignment.
dict.__contains__ does not invoke __missing__, so there's no recursion
risk; in the common non-reentrant case the check is False and behavior
is unchanged.
Unmasks test_defaultdict.TestDefaultDict.test_factory_conflict_with_set_value.
CPython rejects digit-only format-string field names that overflow
Py_ssize_t at parse time with ValueError: Too many decimal digits in
format string (Python/string_parser.c::get_integer). RustPython's
FieldName::parse accepted any digit string usize::from_str could parse,
producing IndexError or KeyError at lookup instead.
Cap the parsed index at isize::MAX (Py_ssize_t::MAX on every platform)
inside FieldName::parse. Also reject digits-only strings whose value
overflows usize itself (caught when parse_usize returns None on an
all-digit input). A new FormatParseError::TooManyDecimalDigits maps to
the byte-identical CPython wording.
Unmasks test_str.StrTest.test_format_huge_item_number.
CPython rejects format-spec widths that exceed Py_ssize_t::MAX with
ValueError: Too many decimal digits in format string. RustPython's
FormatSpec::_parse only capped precision (via parse_precision); width
was accepted up to usize::MAX, so values like sys.maxsize + 1 silently
produced an effectively-ignored width.
Reject any width above i32::MAX with FormatSpecError::DecimalDigitsTooMany,
matching the existing precision cap and producing the byte-identical
ValueError wording.
Unmasks test_str.StrTest.test_format_huge_width.
CPython 3.14 fully removed the long-deprecated fallback from int() to
__trunc__. Classes that want int(x) support must implement __int__ or
__index__; math.trunc(x) continues to use __trunc__.
Delete the __trunc__ branch in PyObject::try_int so a class with only
__trunc__ now reaches the existing TypeError arm, matching CPython.
Unmasks test_int.IntTestCases.test_intconversion.
Closes#7450.
CPython's unicode_new_impl returns the PyObject_Str result as-is when
type == &PyUnicode_Type, only invoking unicode_subtype_new for actual
str subclasses. RustPython's PyStr::Constructor stripped the result via
Self::from(s.as_wtf8().to_owned()) and re-materialized through
into_ref_with_type, dropping the subclass type even when cls is exactly
str.
Add a slot_new branch that returns input.str(vm)? directly when cls is
str_type with no encoding. Subtype construction and the bytes-decoding
path are unchanged.
Unmasks test_str.StrTest.test_conversion (11 assertTypedEqual cases).
CPython's _abc._abc_init validates that __abc_tpflags__ doesn't combine
Py_TPFLAGS_SEQUENCE and Py_TPFLAGS_MAPPING (Modules/_abc.c::_abc_init).
RustPython's check_abc_tpflags copied the bits onto slots.flags without
that validation, so a class declaring both produced an inconsistent
type silently.
Validate the masked __abc_tpflags__ value against COLLECTION_FLAGS and
return a String error (which the existing new_heap_inner caller turns
into a TypeError) when both bits are present. Move the inheritance-skip
check after the self-attrs branch so a child overriding __abc_tpflags__
is still validated even when the parent already set one of the bits.
deque.__reduce__ passed None as the unpickle state, so a deque
subclass's __dict__ and __slots__ values were dropped across a pickle
round-trip. CPython's deque___reduce___impl
(Modules/_collectionsmodule.c::deque___reduce___impl) calls
_PyObject_GetState, which returns the dict (or a (dict, slots) tuple)
so subclass attributes survive.
Replace the placeholder with the result of __getstate__() on the
instance. object.__getstate__ already implements the dict / dict+slots
protocol, matching _PyObject_GetState.
* Enforce int_max_str_digits on int-to-str conversions
The str-to-int direction already enforced sys.get_int_max_str_digits()
via bytes_to_int; the int-to-str direction did not. CPython 3.14 enforces
both per PEP 644.
Adds check_int_to_str_digits helper in builtins::int (bit-count fast path
+ digit upper-bound from log10(2)), wired into the four Python-level
entry points: repr, the str fast path in protocol::object, int.__format__
(decimal/n/empty spec only — binary bases x/o/b are exempt per CPython),
and the DecimalD/I/U branches of vm::cformat for both str % and bytes %.
Unmasks 8 expectedFailure tests across test_int (max_str_digits, DoS
prevention, int_from_other_bases — each mirrored in IntSubclass),
test_ast (test_repr_large_input_crash) and test_reprlib (test_numbers).
Boundary cases (4299/4300/4301 digits at limit=4300) match CPython 3.14.4.
* Skip int-to-str DoS test on platforms without time.get_clock_info
The test_denial_of_service_prevented_int_to_str regression test uses
support.Stopwatch, which calls time.get_clock_info('monotonic'). In
RustPython that function is gated to unix/windows targets only, so on
wasm32-wasip1 it surfaces as AttributeError and breaks the wasm-wasi CI.
Guard the test with skipUnless(hasattr(time, 'get_clock_info'), ...) so
it runs everywhere it can and is skipped on wasm.
Also narrow is_decimal_int_format to Number(Case::Lower): 'N' is rejected
by format_int as UnknownFormatCode, so excluding it preserves that error
path instead of intercepting it with the digit-limit check.
* Add TODO: RUSTPYTHON marker to skipUnless reason
scripts/update_lib uses TODO: RUSTPYTHON markers inside unittest
decorator reason strings to identify and migrate custom RustPython
patches across CPython library updates.
* Use expectedFailureIf for wasm get_clock_info gap
skipUnless silently hides the test forever; expectedFailureIf surfaces
unexpected success once RustPython implements time.get_clock_info on
wasm, prompting marker removal.
The behavior already matched CPython (the slot conflict is detected,
the __doc__ delete is rejected); only the message text drifted.
- "__slots__ conflicts with a class variable" -> drop the stray "a"
to match CPython's "conflicts with class variable".
- "cannot delete '__doc__' attribute of type 'X'" -> insert "immutable"
before "type" to match CPython's wording (CPython surfaces the same
phrase even for user-defined classes since the descriptor refuses
the delete unconditionally).
CPython's staticmethod and classmethod set __func__ and copy wrapper
attributes (__doc__, __name__, etc.) only inside __init__
(Objects/funcobject.c::sm_init / cm_init). RustPython did this work in
slot_new and again in __init__, so subclasses that override __init__
without calling super().__init__() saw __func__ pointing at the
original callable instead of None.
Move the callable assignment and the wrapper-attribute copy into
Initializer::init; slot_new now just validates the signature and stores
None for the callable, matching the CPython contract.
* Report invalid \uXXXX escape position at the u character
CPython's json decoder reports the position of the `u` specifier
when a \uXXXX escape fails to parse, but RustPython was reporting
the preceding `\`. For surrogate-pair cases (\uXXXX\uYYYY) the
second call was passing char_offset + next_char_i + 1, which
lands on the first hex digit of the first escape -- unrelated to
the actual failure site.
Pass next_char_i (position of the primary `u`) to the primary
decode_unicode call, and capture the second `u`'s char index from
the next_tuple peek to pass to the surrogate-pair decode_unicode
call.
Verified: 13 targeted probes across invalid-hex, short, and pair
cases now all match CPython positions. test.test_json 214 tests
pass with no regressions.
* Add regression test for invalid \uXXXX escape position
* Use raise AssertionError instead of assert False (B011)