Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ env:
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_RELEASE_DEBUG: 0
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' # TODO: Remove on 2026/06/02
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # TODO: Remove on 2026/06/02

jobs:
rust_tests:
Expand Down Expand Up @@ -411,6 +411,12 @@ jobs:
key: prek-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/prek

# TODO: Remove on 2026/06/02 when node24 is the default
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
package-manager-cache: false
node-version: "24"

- name: prek
id: prek
uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ repos:
priority: 1 # so rustfmt runs first

- repo: https://github.com/streetsidesoftware/cspell-cli
rev: v9.7.0
rev: v10.0.0
hooks:
- id: cspell
types: [rust]
Expand Down
31 changes: 24 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8"
# ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }
# ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" }

der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] }
phf = { version = "0.13.1", default-features = false, features = ["macros"]}
ahash = "0.8.12"
ascii = "1.1"
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,6 @@ def test_repr(self) -> None:
with self.subTest(test_input=test):
self.assertEqual(repr(ast.parse(test)), snapshot)

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised
def test_repr_large_input_crash(self):
# gh-125010: Fix use-after-free in ast repr()
source = "0x0" + "e" * 10_000
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_deque.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,6 @@ def test_basics(self):
d.clear()
self.assertEqual(len(d), 0)

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'Deque' object has no attribute 'x'
def test_copy_pickle(self):
for cls in Deque, DequeWithSlots:
for d in cls('abc'), cls('abcde', maxlen=4):
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4952,7 +4952,6 @@ class A(int):
with self.assertRaises(TypeError):
a + a

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_slot_shadows_class_variable(self):
with self.assertRaises(ValueError) as cm:
class X:
Expand All @@ -4961,7 +4960,6 @@ class X:
m = str(cm.exception)
self.assertEqual("'foo' in __slots__ conflicts with class variable", m)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_set_doc(self):
class X:
"elephant"
Expand Down Expand Up @@ -5193,7 +5191,6 @@ def foo(self):
with self.assertRaisesRegex(NotImplementedError, "BAR"):
B().foo

@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
def test_staticmethod_new(self):
class MyStaticMethod(staticmethod):
def __init__(self, func):
Expand All @@ -5204,7 +5201,6 @@ def func(): pass
self.assertIsNone(sm.__func__)
self.assertIsNone(sm.__wrapped__)

@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
def test_classmethod_new(self):
class MyClassMethod(classmethod):
def __init__(self, func):
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_int.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import time

import unittest
# TODO: RUSTPYTHON
Expand Down Expand Up @@ -573,7 +574,6 @@ def check(self, i, base=None):
else:
self.int_class(i, base)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_max_str_digits(self):
maxdigits = sys.get_int_max_str_digits()

Expand All @@ -588,7 +588,10 @@ def test_max_str_digits(self):
with self.assertRaises(ValueError):
str(i)

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailureIf(
not hasattr(time, "get_clock_info"),
"TODO: RUSTPYTHON; time.get_clock_info is not available on wasm",
)
def test_denial_of_service_prevented_int_to_str(self):
"""Regression test: ensure we fail before performing O(N**2) work."""
maxdigits = sys.get_int_max_str_digits()
Expand Down Expand Up @@ -713,7 +716,6 @@ def _other_base_helper(self, base):
with self.assertRaises(ValueError) as err:
int_class(f'{s}1', base)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_int_from_other_bases(self):
base = 3
with self.subTest(base=base):
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_reprlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ def test_frozenset(self):
eq(r(frozenset({1, 2, 3, 4, 5, 6})), "frozenset({1, 2, 3, 4, 5, 6})")
eq(r(frozenset({1, 2, 3, 4, 5, 6, 7})), "frozenset({1, 2, 3, 4, 5, 6, ...})")

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_numbers(self):
for x in [123, 1.0 / 3]:
self.assertEqual(r(x), repr(x))
Expand Down
11 changes: 11 additions & 0 deletions crates/common/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,17 @@ impl FormatSpec {
matches!(self.format_type, Some(FormatType::Number(Case::Lower)))
}

/// Returns true if this format spec produces a decimal int representation
/// subject to `sys.get_int_max_str_digits()` (no spec, 'd', or 'n').
/// Binary bases ('b', 'o', 'x', 'X') are exempt per CPython. 'N' is rejected
/// later in `format_int` as `UnknownFormatCode`, so it is not included here.
pub fn is_decimal_int_format(&self) -> bool {
matches!(
self.format_type,
None | Some(FormatType::Decimal) | Some(FormatType::Number(Case::Lower))
)
}

/// Insert locale-aware thousands separators into an integer string.
/// Follows CPython's GroupGenerator logic for variable-width grouping.
fn insert_locale_grouping(int_part: &str, locale: &LocaleInfo) -> String {
Expand Down
2 changes: 1 addition & 1 deletion crates/stdlib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ rustls-pemfile = { version = "2.2", optional = true }
rustls-platform-verifier = { version = "0.7", optional = true }
x509-cert = { version = "0.2.5", features = ["pem", "builder"], optional = true }
x509-parser = { version = "0.18", optional = true }
der = { version = "0.7", features = ["alloc", "oid"], optional = true }
der = { workspace = true, optional = true }
pem-rfc7468 = { version = "1.0", features = ["alloc"], optional = true }
webpki-roots = { version = "1.0", optional = true }
aws-lc-rs = { version = "1.16.3", optional = true }
Expand Down
50 changes: 22 additions & 28 deletions crates/vm/src/builtins/classmethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,34 +66,15 @@ impl Constructor for PyClassMethod {
type Args = PyObjectRef;

fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
let callable: Self::Args = args.bind(vm)?;
// Create a dictionary to hold copied attributes
let dict = vm.ctx.new_dict();

// Copy attributes from the callable to the dict
// This is similar to functools.wraps in CPython
if let Ok(doc) = callable.get_attr("__doc__", vm) {
dict.set_item(identifier!(vm.ctx, __doc__), doc, vm)?;
}
if let Ok(name) = callable.get_attr("__name__", vm) {
dict.set_item(identifier!(vm.ctx, __name__), name, vm)?;
}
if let Ok(qualname) = callable.get_attr("__qualname__", vm) {
dict.set_item(identifier!(vm.ctx, __qualname__), qualname, vm)?;
}
if let Ok(module) = callable.get_attr("__module__", vm) {
dict.set_item(identifier!(vm.ctx, __module__), module, vm)?;
}
if let Ok(annotations) = callable.get_attr("__annotations__", vm) {
dict.set_item(identifier!(vm.ctx, __annotations__), annotations, vm)?;
}

// Create PyClassMethod instance with the pre-populated dict
// Validate the signature here, but defer storing the callable and
// copying its attributes to `__init__` so that subclasses overriding
// `__init__` without calling `super().__init__()` see `__func__` as
// `None`, matching CPython.
let _: Self::Args = args.bind(vm)?;
let classmethod = Self {
callable: PyMutex::new(callable),
callable: PyMutex::new(vm.ctx.none()),
};

let result = PyRef::new_ref(classmethod, cls, Some(dict));
let result = PyRef::new_ref(classmethod, cls, Some(vm.ctx.new_dict()));
Ok(PyObjectRef::from(result))
}

Expand All @@ -105,8 +86,21 @@ impl Constructor for PyClassMethod {
impl Initializer for PyClassMethod {
type Args = PyObjectRef;

fn init(zelf: PyRef<Self>, callable: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
*zelf.callable.lock() = callable;
fn init(zelf: PyRef<Self>, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
*zelf.callable.lock() = callable.clone();
// Copy wrapper attributes from the callable, mirroring functools.wraps.
let dict = zelf.as_object().dict().expect("classmethod has __dict__");
for attr in [
identifier!(vm.ctx, __doc__),
identifier!(vm.ctx, __name__),
identifier!(vm.ctx, __qualname__),
identifier!(vm.ctx, __module__),
identifier!(vm.ctx, __annotations__),
] {
if let Ok(value) = callable.get_attr(attr, vm) {
dict.set_item(attr, value, vm)?;
}
}
Ok(())
}
}
Expand Down
30 changes: 29 additions & 1 deletion crates/vm/src/builtins/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@ impl PyInt {
}
let format_spec =
FormatSpec::parse(spec.as_str()).map_err(|err| err.into_pyexception(vm))?;
if format_spec.is_decimal_int_format() {
check_int_to_str_digits(&zelf.value, vm)?;
}
let result = if format_spec.has_locale_format() {
let locale = crate::format::get_locale_info();
format_spec.format_int_locale(&zelf.value, &locale)
Expand Down Expand Up @@ -655,9 +658,34 @@ impl Comparable for PyInt {
}
}

/// Pre-format check enforcing `sys.get_int_max_str_digits()` on int → str conversions.
/// Mirrors CPython's PEP 644 DoS mitigation. Cheap fast-path for small values via
/// bit-count upper bound on decimal digits.
pub(crate) fn check_int_to_str_digits(value: &BigInt, vm: &VirtualMachine) -> PyResult<()> {
let limit = vm.state.int_max_str_digits.load();
if limit == 0 {
return Ok(());
}
let bits = value.bits();
// Below ~452 decimal digits: definitely under any reasonable limit.
if bits < 1500 {
return Ok(());
}
// Upper bound on decimal digit count: ⌈bits × log10(2)⌉ + 1, with log10(2) ≈ 0.30103.
let digits_upper = (bits as usize * 30103 / 100000) + 1;
if digits_upper > limit {
return Err(vm.new_value_error(format!(
"Exceeds the limit ({limit} digits) for integer string conversion; \
use sys.set_int_max_str_digits() to increase the limit"
)));
}
Ok(())
}

impl Representable for PyInt {
#[inline]
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
check_int_to_str_digits(&zelf.value, vm)?;
Ok(zelf.to_str_radix_10())
}
}
Expand Down
Loading
Loading