From 2e62cac72b81b5eb3a937f58c6e54d09466116c4 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Thu, 5 Feb 2026 20:57:09 +0900 Subject: [PATCH] Implement more warnings --- .cspell.dict/python-more.txt | 53 ++-- Lib/test/test_site.py | 1 - Lib/test/test_warnings/__init__.py | 26 -- crates/stdlib/src/_asyncio.rs | 8 +- crates/stdlib/src/socket.rs | 2 +- crates/vm/src/coroutine.rs | 8 +- crates/vm/src/frame.rs | 19 +- crates/vm/src/stdlib/ast/python.rs | 4 +- crates/vm/src/stdlib/ast/string.rs | 2 +- crates/vm/src/stdlib/warnings.rs | 186 +++++++++++- crates/vm/src/vm/context.rs | 3 + crates/vm/src/warn.rs | 467 ++++++++++++++++++++--------- src/settings.rs | 11 + 13 files changed, 570 insertions(+), 220 deletions(-) diff --git a/.cspell.dict/python-more.txt b/.cspell.dict/python-more.txt index 2dd31f8f579..2ce5d246d72 100644 --- a/.cspell.dict/python-more.txt +++ b/.cspell.dict/python-more.txt @@ -1,8 +1,10 @@ abiflags abstractmethods +addcompare aenter aexit aiter +altzone anext anextawaitable annotationlib @@ -24,6 +26,7 @@ breakpointhook cformat chunksize classcell +classmethods closefd closesocket codepoint @@ -32,6 +35,8 @@ codesize contextvar cpython cratio +ctype +ctypes dealloc debugbuild decompressor @@ -74,6 +79,8 @@ fstring fstrings ftruncate genexpr +genexpressions +getargs getattro getcodesize getdefaultencoding @@ -83,14 +90,17 @@ getformat getframe getframemodulename getnewargs +getopt getpip getrandom getrecursionlimit getrefcount getsizeof getswitchinterval +getweakref getweakrefcount getweakrefs +getweakrefs getwindowsversion gmtoff groupdict @@ -103,8 +113,12 @@ idxs impls indexgroup infj +inittab +Inittab instancecheck instanceof +interpchannels +interpqueues irepeat isabstractmethod isbytes @@ -129,6 +143,7 @@ listcomp longrange lvalue mappingproxy +markupbase maskpri maxdigits MAXGROUPS @@ -144,6 +159,7 @@ mformat mro mros multiarch +mymodule namereplace nanj nbytes @@ -156,6 +172,7 @@ nlocals NOARGS nonbytes Nonprintable +onceregistry origname ospath pendingcr @@ -170,7 +187,10 @@ profilefunc pycache pycodecs pycs +pydatetime pyexpat +pyio +pymain PYTHONAPI PYTHONBREAKPOINT PYTHONDEBUG @@ -220,10 +240,13 @@ scproxy seennl setattro setcomp +setprofileallthreads setrecursionlimit setswitchinterval +settraceallthreads showwarnmsg signum +sitebuiltins slotnames STACKLESS stacklevel @@ -232,14 +255,17 @@ startpos subclassable subclasscheck subclasshook +subclassing suboffset suboffsets SUBPATTERN +subpatterns sumprod surrogateescape surrogatepass sysconf sysconfigdata +sysdict sysvars teedata thisclass @@ -266,35 +292,10 @@ warnopts weaklist weakproxy weakrefs +weakrefset winver withdata xmlcharrefreplace xoptions xopts yieldfrom -addcompare -altzone -classmethods -ctype -ctypes -genexpressions -getargs -getopt -getweakref -getweakrefs -inittab -Inittab -interpchannels -interpqueues -markupbase -mymodule -pydatetime -pyio -pymain -setprofileallthreads -settraceallthreads -sitebuiltins -subclassing -subpatterns -sysdict -weakrefset diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 56ed457882c..01951e6247b 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -591,7 +591,6 @@ def test_lazy_imports(self): class StartupImportTests(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON @support.requires_subprocess() def test_startup_imports(self): # Get sys.path in isolated mode (python3 -I) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 83a84f6871a..16703835806 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -807,7 +807,6 @@ class CWarnTests(WarnTests, unittest.TestCase): # As an early adopter, we sanity check the # test.import_helper.import_fresh_module utility function - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'function' object has unexpected attribute '__code__' def test_accelerated(self): self.assertIsNot(original_warnings, self.module) self.assertNotHasAttr(self.module.warn, '__code__') @@ -1012,7 +1011,6 @@ def test_showwarning_missing(self): result = stream.getvalue() self.assertIn(text, result) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module 'warnings' has no attribute '_showwarnmsg'. Did you mean: 'showwarning'? def test_showwarnmsg_missing(self): # Test that _showwarnmsg() missing is okay. text = 'del _showwarnmsg test' @@ -1458,7 +1456,6 @@ class PyCatchWarningTests(CatchWarningTests, unittest.TestCase): class EnvironmentVariableTests(BaseTest): - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'[]' != b"['ignore::DeprecationWarning']" def test_single_warning(self): rc, stdout, stderr = assert_python_ok("-c", "import sys; sys.stdout.write(str(sys.warnoptions))", @@ -1466,7 +1463,6 @@ def test_single_warning(self): PYTHONDEVMODE="") self.assertEqual(stdout, b"['ignore::DeprecationWarning']") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'[]' != b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']" def test_comma_separated_warnings(self): rc, stdout, stderr = assert_python_ok("-c", "import sys; sys.stdout.write(str(sys.warnoptions))", @@ -1475,7 +1471,6 @@ def test_comma_separated_warnings(self): self.assertEqual(stdout, b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']") - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b"['ignore::UnicodeWarning']" != b"['ignore::DeprecationWarning', 'ignore::UnicodeWarning']" @force_not_colorized def test_envvar_and_command_line(self): rc, stdout, stderr = assert_python_ok("-Wignore::UnicodeWarning", "-c", @@ -1535,7 +1530,6 @@ def test_default_filter_configuration(self): self.assertEqual(stdout_lines, expected_output) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'[]' != b"['ignore:DeprecationWarning\xc3\xa6']" @unittest.skipUnless(sys.getfilesystemencoding() != 'ascii', 'requires non-ascii filesystemencoding') def test_nonascii(self): @@ -1550,10 +1544,6 @@ def test_nonascii(self): class CEnvironmentVariableTests(EnvironmentVariableTests, unittest.TestCase): module = c_warnings - @unittest.expectedFailure # TODO: RUSTPYTHON; Lists differ - def test_default_filter_configuration(self): - return super().test_default_filter_configuration() - class PyEnvironmentVariableTests(EnvironmentVariableTests, unittest.TestCase): module = py_warnings @@ -1851,7 +1841,6 @@ def h(x): self.assertEqual(len(overloads), 2) self.assertEqual(overloads[0].__deprecated__, "no more ints") - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_class(self): @deprecated("A will go away soon") class A: @@ -1863,7 +1852,6 @@ class A: with self.assertRaises(TypeError): A(42) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_class_with_init(self): @deprecated("HasInit will go away soon") class HasInit: @@ -1874,7 +1862,6 @@ def __init__(self, x): instance = HasInit(42) self.assertEqual(instance.x, 42) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_class_with_new(self): has_new_called = False @@ -1893,7 +1880,6 @@ def __init__(self, x) -> None: self.assertEqual(instance.x, 42) self.assertTrue(has_new_called) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_class_with_inherited_new(self): new_base_called = False @@ -1915,7 +1901,6 @@ class HasInheritedNew(NewBase): self.assertEqual(instance.x, 42) self.assertTrue(new_base_called) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_class_with_new_but_no_init(self): new_called = False @@ -1933,7 +1918,6 @@ def __new__(cls, x): self.assertEqual(instance.x, 42) self.assertTrue(new_called) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_mixin_class(self): @deprecated("Mixin will go away soon") class Mixin: @@ -1950,7 +1934,6 @@ class Child(Base, Mixin): instance = Child(42) self.assertEqual(instance.a, 42) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_do_not_shadow_user_arguments(self): new_called = False new_called_cls = None @@ -1970,7 +1953,6 @@ class Foo(metaclass=MyMeta, cls='haha'): self.assertTrue(new_called) self.assertEqual(new_called_cls, 'haha') - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_existing_init_subclass(self): @deprecated("C will go away soon") class C: @@ -1987,7 +1969,6 @@ class D(C): self.assertTrue(D.inited) self.assertIsInstance(D(), D) # no deprecation - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_existing_init_subclass_in_base(self): class Base: def __init_subclass__(cls, x) -> None: @@ -2008,7 +1989,6 @@ class D(C, x=3): self.assertEqual(D.inited, 3) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_existing_init_subclass_in_sibling_base(self): @deprecated("A will go away soon") class A: @@ -2028,7 +2008,6 @@ class D(B, A, x=42): pass self.assertEqual(D.inited, 42) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_init_subclass_has_correct_cls(self): init_subclass_saw = None @@ -2046,7 +2025,6 @@ class C(Base): self.assertIs(init_subclass_saw, C) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_init_subclass_with_explicit_classmethod(self): init_subclass_saw = None @@ -2065,7 +2043,6 @@ class C(Base): self.assertIs(init_subclass_saw, C) - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_function(self): @deprecated("b will go away soon") def b(): @@ -2074,7 +2051,6 @@ def b(): with self.assertWarnsRegex(DeprecationWarning, "b will go away soon"): b() - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_method(self): class Capybara: @deprecated("x will go away soon") @@ -2085,7 +2061,6 @@ def x(self): with self.assertWarnsRegex(DeprecationWarning, "x will go away soon"): instance.x() - @unittest.expectedFailure # TODO: RUSTPYTHON; DeprecationWarning not triggered def test_property(self): class Capybara: @property @@ -2113,7 +2088,6 @@ def no_more_setting(self, value): with self.assertWarnsRegex(DeprecationWarning, "no more setting"): instance.no_more_setting = 42 - @unittest.expectedFailure # TODO: RUSTPYTHON; RuntimeWarning not triggered def test_category(self): @deprecated("c will go away soon", category=RuntimeWarning) def c(): diff --git a/crates/stdlib/src/_asyncio.rs b/crates/stdlib/src/_asyncio.rs index 6e7a8c6e0e5..cc2e78bc35f 100644 --- a/crates/stdlib/src/_asyncio.rs +++ b/crates/stdlib/src/_asyncio.rs @@ -1014,10 +1014,12 @@ pub(crate) mod _asyncio { // Warn about deprecated (type, val, tb) signature if exc_val.is_present() || exc_tb.is_present() { warn::warn( - vm.ctx.new_str( - "the (type, val, tb) signature of throw() is deprecated, \ + vm.ctx + .new_str( + "the (type, val, tb) signature of throw() is deprecated, \ use throw(val) instead", - ), + ) + .into(), Some(vm.ctx.exceptions.deprecation_warning.to_owned()), 1, None, diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 0d67d3680ad..32fba216e97 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -2144,7 +2144,7 @@ mod _socket { laddr ); let _ = crate::vm::warn::warn( - vm.ctx.new_str(msg), + vm.ctx.new_str(msg).into(), Some(vm.ctx.exceptions.resource_warning.to_owned()), 1, None, diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 67325283f3a..e2dd849c161 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -300,10 +300,12 @@ pub fn warn_deprecated_throw_signature( ) -> PyResult<()> { if exc_val.is_present() || exc_tb.is_present() { crate::warn::warn( - vm.ctx.new_str( - "the (type, val, tb) signature of throw() is deprecated, \ + vm.ctx + .new_str( + "the (type, val, tb) signature of throw() is deprecated, \ use throw(val) instead", - ), + ) + .into(), Some(vm.ctx.exceptions.deprecation_warning.to_owned()), 1, None, diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index da918af18c8..034dd8ce7f0 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -328,19 +328,14 @@ impl Py { } pub fn next_external_frame(&self, vm: &VirtualMachine) -> Option { - self.f_back(vm).map(|mut back| { - loop { - back = if let Some(back) = back.to_owned().f_back(vm) { - back - } else { - break back; - }; - - if !back.is_internal_frame() { - break back; - } + let mut frame = self.f_back(vm); + while let Some(ref f) = frame { + if !f.is_internal_frame() { + break; } - }) + frame = f.f_back(vm); + } + frame } } diff --git a/crates/vm/src/stdlib/ast/python.rs b/crates/vm/src/stdlib/ast/python.rs index 772240451e5..ab21fb8f0dc 100644 --- a/crates/vm/src/stdlib/ast/python.rs +++ b/crates/vm/src/stdlib/ast/python.rs @@ -336,7 +336,7 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt key )); warn::warn( - message, + message.into(), Some(vm.ctx.exceptions.deprecation_warning.to_owned()), 1, None, @@ -387,7 +387,7 @@ Support for arbitrary keyword arguments is deprecated and will be removed in Pyt field.as_str() )); warn::warn( - message, + message.into(), Some(vm.ctx.exceptions.deprecation_warning.to_owned()), 1, None, diff --git a/crates/vm/src/stdlib/ast/string.rs b/crates/vm/src/stdlib/ast/string.rs index bfeaad82f9c..4b6a6e8489f 100644 --- a/crates/vm/src/stdlib/ast/string.rs +++ b/crates/vm/src/stdlib/ast/string.rs @@ -224,7 +224,7 @@ fn warn_invalid_escape_sequences_in_format_spec( "\"\\{next}\" is an invalid escape sequence. Such sequences will not work in the future. Did you mean \"\\\\{next}\"? A raw string is also an option." )); let _ = warn::warn( - message, + message.into(), Some(vm.ctx.exceptions.syntax_warning.to_owned()), 1, None, diff --git a/crates/vm/src/stdlib/warnings.rs b/crates/vm/src/stdlib/warnings.rs index 198df07d6c0..1725fefd2a8 100644 --- a/crates/vm/src/stdlib/warnings.rs +++ b/crates/vm/src/stdlib/warnings.rs @@ -20,29 +20,195 @@ pub fn warn( #[pymodule] mod _warnings { use crate::{ - PyResult, VirtualMachine, - builtins::{PyStrRef, PyTypeRef}, + AsObject, PyObjectRef, PyResult, VirtualMachine, + builtins::{PyDictRef, PyListRef, PyStrRef, PyTupleRef, PyTypeRef}, + convert::TryFromObject, function::OptionalArg, }; + #[pyattr] + fn filters(vm: &VirtualMachine) -> PyListRef { + vm.state.warnings.filters.clone() + } + + #[pyattr(name = "_defaultaction")] + fn default_action(vm: &VirtualMachine) -> PyStrRef { + vm.state.warnings.default_action.clone() + } + + #[pyattr(name = "_onceregistry")] + fn once_registry(vm: &VirtualMachine) -> PyDictRef { + vm.state.warnings.once_registry.clone() + } + + #[pyattr(name = "_warnings_context")] + fn warnings_context(vm: &VirtualMachine) -> PyObjectRef { + vm.state + .warnings + .context_var + .get_or_init(|| { + // Try to create a real ContextVar if _contextvars is available. + // During early startup it may not be importable yet, in which + // case we fall back to None. This is safe because + // context_aware_warnings defaults to False. + if let Ok(contextvars) = vm.import("_contextvars", 0) + && let Ok(cv_cls) = contextvars.get_attr("ContextVar", vm) + && let Ok(cv) = cv_cls.call(("_warnings_context",), vm) + { + cv + } else { + vm.ctx.none() + } + }) + .clone() + } + + #[pyfunction(name = "_acquire_lock")] + fn acquire_lock(vm: &VirtualMachine) { + vm.state.warnings.acquire_lock(); + } + + #[pyfunction(name = "_release_lock")] + fn release_lock(vm: &VirtualMachine) -> PyResult<()> { + if !vm.state.warnings.release_lock() { + return Err(vm.new_runtime_error("cannot release un-acquired lock".to_owned())); + } + Ok(()) + } + + #[pyfunction(name = "_filters_mutated_lock_held")] + fn filters_mutated_lock_held(vm: &VirtualMachine) { + vm.state.warnings.filters_mutated(); + } + #[derive(FromArgs)] struct WarnArgs { #[pyarg(positional)] - message: PyStrRef, + message: PyObjectRef, #[pyarg(any, optional)] - category: OptionalArg, + category: OptionalArg, #[pyarg(any, optional)] - stacklevel: OptionalArg, + stacklevel: OptionalArg, + #[pyarg(named, optional)] + source: OptionalArg, + #[pyarg(named, optional)] + skip_file_prefixes: OptionalArg, + } + + /// Validate and resolve the category argument, matching get_category() in C. + fn get_category( + message: &PyObjectRef, + category: Option, + vm: &VirtualMachine, + ) -> PyResult> { + let cat_obj = match category { + Some(c) if !vm.is_none(&c) => c, + _ => { + if message.fast_isinstance(vm.ctx.exceptions.warning) { + return Ok(Some(message.class().to_owned())); + } else { + return Ok(None); // will default to UserWarning in warn_explicit + } + } + }; + + let cat = PyTypeRef::try_from_object(vm, cat_obj.clone()).map_err(|_| { + vm.new_type_error(format!( + "category must be a Warning subclass, not '{}'", + cat_obj.class().name() + )) + })?; + + if !cat.fast_issubclass(vm.ctx.exceptions.warning) { + return Err(vm.new_type_error(format!( + "category must be a Warning subclass, not '{}'", + cat.class().name() + ))); + } + + Ok(Some(cat)) } #[pyfunction] fn warn(args: WarnArgs, vm: &VirtualMachine) -> PyResult<()> { - let level = args.stacklevel.unwrap_or(1); - crate::warn::warn( + let level = args.stacklevel.unwrap_or(1) as isize; + + let category = get_category(&args.message, args.category.into_option(), vm)?; + + // Validate skip_file_prefixes: each element must be a str + let skip_prefixes = args.skip_file_prefixes.into_option(); + if let Some(ref prefixes) = skip_prefixes { + for item in prefixes.iter() { + if !item.class().is(vm.ctx.types.str_type) { + return Err( + vm.new_type_error("skip_file_prefixes must be a tuple of strs".to_owned()) + ); + } + } + } + + crate::warn::warn_with_skip( + args.message, + category, + level, + args.source.into_option(), + skip_prefixes, + vm, + ) + } + + #[derive(FromArgs)] + struct WarnExplicitArgs { + #[pyarg(positional)] + message: PyObjectRef, + #[pyarg(positional)] + category: PyObjectRef, + #[pyarg(positional)] + filename: PyStrRef, + #[pyarg(positional)] + lineno: usize, + #[pyarg(any, optional)] + module: OptionalArg, + #[pyarg(any, optional)] + registry: OptionalArg, + #[pyarg(any, optional)] + module_globals: OptionalArg, + #[pyarg(named, optional)] + source: OptionalArg, + } + + #[pyfunction] + fn warn_explicit(args: WarnExplicitArgs, vm: &VirtualMachine) -> PyResult<()> { + let registry = args.registry.into_option().unwrap_or_else(|| vm.ctx.none()); + + let module = args.module.into_option(); + + // Validate module_globals: must be None or a dict + if let Some(ref mg) = args.module_globals.into_option() + && !vm.is_none(mg) + && !mg.class().is(vm.ctx.types.dict_type) + { + return Err(vm.new_type_error("module_globals must be a dict".to_owned())); + } + + let category = + if vm.is_none(&args.category) { + None + } else { + Some(PyTypeRef::try_from_object(vm, args.category).map_err(|_| { + vm.new_type_error("category must be a Warning subclass".to_owned()) + })?) + }; + + crate::warn::warn_explicit( + category, args.message, - args.category.into_option(), - level as isize, - None, + args.filename, + args.lineno, + module, + registry, + None, // source_line + args.source.into_option(), vm, ) } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 24e52608016..d2ae3a8acff 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -244,7 +244,10 @@ declare_const_name! { // common names _attributes, _fields, + _defaultaction, + _onceregistry, _showwarnmsg, + filters, backslashreplace, close, copy, diff --git a/crates/vm/src/warn.rs b/crates/vm/src/warn.rs index 3ec75090b4e..c0046b08eab 100644 --- a/crates/vm/src/warn.rs +++ b/crates/vm/src/warn.rs @@ -1,27 +1,69 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyResult, VirtualMachine, builtins::{ - PyDictRef, PyListRef, PyStr, PyStrInterned, PyStrRef, PyTuple, PyTupleRef, PyTypeRef, + PyBaseExceptionRef, PyDictRef, PyListRef, PyStr, PyStrInterned, PyStrRef, PyTuple, + PyTupleRef, PyTypeRef, }, - convert::{IntoObject, TryFromObject}, - types::PyComparisonOp, + convert::TryFromObject, }; +use core::sync::atomic::{AtomicUsize, Ordering}; +use rustpython_common::lock::OnceCell; pub struct WarningsState { - filters: PyListRef, - _once_registry: PyDictRef, - default_action: PyStrRef, - filters_version: usize, + pub filters: PyListRef, + pub once_registry: PyDictRef, + pub default_action: PyStrRef, + pub filters_version: AtomicUsize, + pub context_var: OnceCell, + lock_count: AtomicUsize, } impl WarningsState { - fn create_filter(ctx: &Context) -> PyListRef { + fn create_default_filters(ctx: &Context) -> PyListRef { + // Default filters matching _Py_InitWarningsFilters. + // The module field uses plain strings (not regex); check_matched handles + // both plain strings (exact comparison) and regex objects (.match()). ctx.new_list(vec![ ctx.new_tuple(vec![ + ctx.new_str("default").into(), + ctx.none(), + ctx.exceptions.deprecation_warning.as_object().to_owned(), ctx.new_str("__main__").into(), - ctx.types.none_type.as_object().to_owned(), - ctx.exceptions.warning.as_object().to_owned(), - ctx.new_str("ACTION").into(), + ctx.new_int(0).into(), + ]) + .into(), + ctx.new_tuple(vec![ + ctx.new_str("ignore").into(), + ctx.none(), + ctx.exceptions.deprecation_warning.as_object().to_owned(), + ctx.none(), + ctx.new_int(0).into(), + ]) + .into(), + ctx.new_tuple(vec![ + ctx.new_str("ignore").into(), + ctx.none(), + ctx.exceptions + .pending_deprecation_warning + .as_object() + .to_owned(), + ctx.none(), + ctx.new_int(0).into(), + ]) + .into(), + ctx.new_tuple(vec![ + ctx.new_str("ignore").into(), + ctx.none(), + ctx.exceptions.import_warning.as_object().to_owned(), + ctx.none(), + ctx.new_int(0).into(), + ]) + .into(), + ctx.new_tuple(vec![ + ctx.new_str("ignore").into(), + ctx.none(), + ctx.exceptions.resource_warning.as_object().to_owned(), + ctx.none(), ctx.new_int(0).into(), ]) .into(), @@ -30,25 +72,53 @@ impl WarningsState { pub fn init_state(ctx: &Context) -> Self { Self { - filters: Self::create_filter(ctx), - _once_registry: ctx.new_dict(), + filters: Self::create_default_filters(ctx), + once_registry: ctx.new_dict(), default_action: ctx.new_str("default"), - filters_version: 0, + filters_version: AtomicUsize::new(0), + context_var: OnceCell::new(), + lock_count: AtomicUsize::new(0), } } + + pub fn acquire_lock(&self) { + self.lock_count.fetch_add(1, Ordering::SeqCst); + } + + pub fn release_lock(&self) -> bool { + let prev = self.lock_count.load(Ordering::SeqCst); + if prev == 0 { + return false; + } + self.lock_count.fetch_sub(1, Ordering::SeqCst); + true + } + + pub fn filters_mutated(&self) { + self.filters_version.fetch_add(1, Ordering::SeqCst); + } } +/// Match a filter field against an argument. +/// - None matches everything +/// - Plain strings do exact comparison +/// - Regex objects use .match() method fn check_matched(obj: &PyObject, arg: &PyObject, vm: &VirtualMachine) -> PyResult { - if obj.class().is(vm.ctx.types.none_type) { + if vm.is_none(obj) { return Ok(true); } - if obj.rich_compare_bool(arg, PyComparisonOp::Eq, vm)? { - return Ok(false); + // Plain string: exact comparison + if obj.class().is(vm.ctx.types.str_type) { + let result = obj.rich_compare_bool(arg, crate::types::PyComparisonOp::Eq, vm)?; + return Ok(result); } - let result = obj.call((arg.to_owned(),), vm); - Ok(result.is_ok()) + // Regex or other object: call .match() method + match vm.call_method(obj, "match", (arg.to_owned(),)) { + Ok(result) => Ok(result.is_true(vm)?), + Err(_) => Ok(false), + } } fn get_warnings_attr( @@ -68,7 +138,6 @@ fn get_warnings_attr( } } else { // Check sys.modules for already-imported warnings module - // This is what CPython does with PyImport_GetModule match vm.sys_module.get_attr(identifier!(vm, modules), vm) { Ok(modules) => match modules.get_item(vm.ctx.intern_str("warnings"), vm) { Ok(module) => module, @@ -77,62 +146,102 @@ fn get_warnings_attr( Err(_) => return Ok(None), } }; - Ok(Some(module.get_attr(attr_name, vm)?)) + match module.get_attr(attr_name, vm) { + Ok(attr) => Ok(Some(attr)), + Err(_) => Ok(None), + } } +/// Get the warnings filters list from sys.modules['warnings'].filters, +/// falling back to vm.state.warnings.filters. +fn get_warnings_filters(vm: &VirtualMachine) -> PyResult { + if let Ok(Some(filters_obj)) = get_warnings_attr(vm, identifier!(&vm.ctx, filters), false) + && let Ok(filters) = filters_obj.try_into_value::(vm) + { + return Ok(filters); + } + Ok(vm.state.warnings.filters.clone()) +} + +/// Get the default action from sys.modules['warnings']._defaultaction, +/// falling back to vm.state.warnings.default_action. +fn get_default_action(vm: &VirtualMachine) -> PyResult { + if let Ok(Some(action)) = get_warnings_attr(vm, identifier!(&vm.ctx, _defaultaction), false) { + return Ok(action); + } + Ok(vm.state.warnings.default_action.clone().into()) +} + +/// Get the once registry from sys.modules['warnings']._onceregistry, +/// falling back to vm.state.warnings.once_registry. +fn get_once_registry(vm: &VirtualMachine) -> PyResult { + if let Ok(Some(registry)) = get_warnings_attr(vm, identifier!(&vm.ctx, _onceregistry), false) { + return Ok(registry); + } + Ok(vm.state.warnings.once_registry.clone().into()) +} + +/// Called from Rust code to issue a warning via the Python warnings module. pub fn warn( - message: PyStrRef, + message: PyObjectRef, category: Option, stack_level: isize, source: Option, vm: &VirtualMachine, ) -> PyResult<()> { - let (filename, lineno, module, registry) = setup_context(stack_level, vm)?; + warn_with_skip(message, category, stack_level, source, None, vm) +} + +/// warn() with skip_file_prefixes support. +pub fn warn_with_skip( + message: PyObjectRef, + category: Option, + mut stack_level: isize, + source: Option, + skip_file_prefixes: Option, + vm: &VirtualMachine, +) -> PyResult<()> { + // When skip_file_prefixes is active and non-empty, clamp stacklevel to at least 2. + if let Some(ref prefixes) = skip_file_prefixes + && !prefixes.is_empty() + && stack_level < 2 + { + stack_level = 2; + } + let (filename, lineno, module, registry) = + setup_context(stack_level, skip_file_prefixes.as_ref(), vm)?; warn_explicit( category, message, filename, lineno, module, registry, None, source, vm, ) } -fn get_default_action(vm: &VirtualMachine) -> PyResult { - Ok(vm.state.warnings.default_action.clone().into()) - // .map_err(|_| { - // vm.new_value_error(format!( - // "_warnings.defaultaction must be a string, not '{}'", - // vm.state.warnings.default_action - // )) - // }) -} - fn get_filter( category: PyObjectRef, text: PyObjectRef, lineno: usize, module: PyObjectRef, - mut _item: PyTupleRef, vm: &VirtualMachine, ) -> PyResult { - let filters = vm.state.warnings.filters.as_object().to_owned(); - - let filters: PyListRef = filters - .try_into_value(vm) - .map_err(|_| vm.new_value_error("_warnings.filters must be a list"))?; - - /* WarningsState.filters could change while we are iterating over it. */ - for i in 0..filters.borrow_vec().len() { - let tmp_item = if let Some(tmp_item) = filters.borrow_vec().get(i).cloned() { - let tmp_item = PyTupleRef::try_from_object(vm, tmp_item)?; - (tmp_item.len() == 5).then_some(tmp_item) - } else { - None - } - .ok_or_else(|| vm.new_value_error(format!("_warnings.filters item {i} isn't a 5-tuple")))?; + let filters = get_warnings_filters(vm)?; + + // filters could change while we are iterating over it. + // Re-check list length each iteration (matches C behavior). + let mut i = 0; + while i < filters.borrow_vec().len() { + let Some(tmp_item) = filters.borrow_vec().get(i).cloned() else { + break; + }; + let tmp_item = PyTupleRef::try_from_object(vm, tmp_item.clone()) + .ok() + .filter(|t| t.len() == 5) + .ok_or_else(|| { + vm.new_value_error(format!("_warnings.filters item {i} isn't a 5-tuple")) + })?; /* Python code: action, msg, cat, mod, ln = item */ - let action = if let Some(action) = tmp_item.first() { - action.str_utf8(vm).map(|action| action.into_object()) - } else { - Err(vm.new_type_error("action must be a string")) - }; + let action = tmp_item + .first() + .ok_or_else(|| vm.new_type_error("action must be a string".to_owned()))?; let good_msg = if let Some(msg) = tmp_item.get(1) { check_matched(msg, &text, vm)? @@ -141,7 +250,7 @@ fn get_filter( }; let is_subclass = if let Some(cat) = tmp_item.get(2) { - category.fast_isinstance(cat.class()) + category.is_subclass(cat, vm)? } else { false }; @@ -157,54 +266,58 @@ fn get_filter( }); if good_msg && good_mod && is_subclass && (ln == 0 || lineno == ln) { - _item = tmp_item; - return action; + return Ok(action.to_owned()); } + i += 1; } get_default_action(vm) } fn already_warned( - registry: PyObjectRef, + registry: &PyObject, key: PyObjectRef, should_set: bool, vm: &VirtualMachine, ) -> PyResult { + if vm.is_none(registry) { + return Ok(false); + } + + let current_version = vm.state.warnings.filters_version.load(Ordering::SeqCst); let version_obj = registry.get_item(identifier!(&vm.ctx, version), vm).ok(); - let filters_version = vm.ctx.new_int(vm.state.warnings.filters_version).into(); - match version_obj { - Some(version_obj) - if version_obj.try_int(vm).is_ok() || version_obj.is(&filters_version) => + let version_matches = version_obj.as_ref().is_some_and(|v| { + v.try_int(vm) + .map(|i| i.as_u32_mask() as usize == current_version) + .unwrap_or(false) + }); + + if version_matches { + // Version matches: check if key is already in the registry + if let Ok(val) = registry.get_item(key.as_ref(), vm) + && val.is_true(vm)? { - // Use .ok() to handle KeyError when key doesn't exist (like Python's dict.get()) - if let Ok(already_warned) = registry.get_item(key.as_ref(), vm) - && already_warned.is_true(vm)? - { - return Ok(true); - } + return Ok(true); // was already warned } - _ => { - let registry = registry.dict(); - if let Some(registry) = registry.as_ref() { - registry.clear(); - let r = registry.set_item("version", filters_version, vm); - if r.is_err() { - return Ok(false); - } - } + } else { + // Version mismatch or missing: clear registry and set new version + if let Ok(dict) = PyDictRef::try_from_object(vm, registry.to_owned()) { + dict.clear(); + let _ = dict.set_item( + identifier!(&vm.ctx, version), + vm.ctx.new_int(current_version).into(), + vm, + ); } } - /* This warning wasn't found in the registry, set it. */ if !should_set { return Ok(false); } - let item = vm.ctx.true_value.clone().into(); - let _ = registry.set_item(key.as_ref(), item, vm); // ignore set error - Ok(true) + let _ = registry.set_item(key.as_ref(), vm.ctx.true_value.clone().into(), vm); + Ok(false) // was NOT previously warned (but now it's recorded) } fn normalize_module(filename: &Py, vm: &VirtualMachine) -> Option { @@ -218,10 +331,11 @@ fn normalize_module(filename: &Py, vm: &VirtualMachine) -> Option, - message: PyStrRef, + message: PyObjectRef, filename: PyStrRef, lineno: usize, module: Option, @@ -230,9 +344,29 @@ fn warn_explicit( source: Option, vm: &VirtualMachine, ) -> PyResult<()> { - let registry: PyObjectRef = registry - .try_into_value(vm) - .map_err(|_| vm.new_type_error("'registry' must be a dict or None"))?; + // Determine text and category based on whether message is a Warning instance + let is_warning = message.fast_isinstance(vm.ctx.exceptions.warning); + + let (text, category) = if is_warning { + let text = message.str(vm)?; + let cat = message.class().to_owned(); + (text, cat) + } else { + // For non-Warning messages, convert to string via str() + let text = message.str(vm)?; + let cat = if let Some(category) = category { + if !category.fast_issubclass(vm.ctx.exceptions.warning) { + return Err(vm.new_type_error(format!( + "category must be a Warning subclass, not '{}'", + category.class().name() + ))); + } + category + } else { + vm.ctx.exceptions.user_warning.to_owned() + }; + (text, cat) + }; // Normalize module. let module = match module.or_else(|| normalize_module(&filename, vm)) { @@ -240,76 +374,110 @@ fn warn_explicit( None => return Ok(()), }; - // Normalize message. - let text = message.as_wtf8(); - - let category = if let Some(category) = category { - if !category.fast_issubclass(vm.ctx.exceptions.warning) { - return Err(vm.new_type_error(format!( - "category must be a Warning subclass, not '{}'", - category.class().name() - ))); - } - category - } else { - vm.ctx.exceptions.user_warning.to_owned() - }; - - let category = if message.fast_isinstance(vm.ctx.exceptions.warning) { - message.class().to_owned() - } else { - category - }; - - // Create key. - let key = PyTuple::new_ref( + // Create key: (text, category, lineno) - used for "default" and "module" actions + let key: PyObjectRef = PyTuple::new_ref( vec![ - vm.ctx.new_int(3).into(), - vm.ctx.new_str(text).into(), + text.clone().into(), category.as_object().to_owned(), vm.ctx.new_int(lineno).into(), ], &vm.ctx, - ); + ) + .into(); - if !vm.is_none(registry.as_object()) && already_warned(registry, key.into_object(), false, vm)? - { + // Check if already warned + if !vm.is_none(®istry) && already_warned(®istry, key.clone(), false, vm)? { return Ok(()); } - let item = vm.ctx.new_tuple(vec![]); + // Get filter action let action = get_filter( category.as_object().to_owned(), - vm.ctx.new_str(text).into(), + text.clone().into(), lineno, module, - item, vm, )?; - if action.str_utf8(vm)?.as_str().eq("error") { - return Err(vm.new_type_error(message.to_string())); - } + let action_str = PyStrRef::try_from_object(vm, action) + .map_err(|_| vm.new_type_error("action must be a string".to_owned()))?; - if action.str_utf8(vm)?.as_str().eq("ignore") { - return Ok(()); + match action_str.as_str() { + "error" => { + // Raise the Warning as an exception + let exc = if is_warning { + PyBaseExceptionRef::try_from_object(vm, message)? + } else { + vm.invoke_exception(category.clone(), vec![text.into()])? + }; + return Err(exc); + } + "ignore" => return Ok(()), + "once" => { + // "once" uses (text, category) as key — no lineno + let once_key: PyObjectRef = PyTuple::new_ref( + vec![text.clone().into(), category.as_object().to_owned()], + &vm.ctx, + ) + .into(); + let reg = get_once_registry(vm)?; + if already_warned(®, once_key, true, vm)? { + return Ok(()); // already warned once + } + } + "always" | "all" => { /* fall through to show warning */ } + "module" => { + if !vm.is_none(®istry) { + // Record with the full key (text, category, lineno) + already_warned(®istry, key, true, vm)?; + // Check/set altkey (text, category) — without lineno. + // If the altkey is already recorded, suppress. + let alt_key: PyObjectRef = PyTuple::new_ref( + vec![text.clone().into(), category.as_object().to_owned()], + &vm.ctx, + ) + .into(); + if already_warned(®istry, alt_key, true, vm)? { + return Ok(()); + } + } + } + "default" => { + if !vm.is_none(®istry) && already_warned(®istry, key, true, vm)? { + return Ok(()); + } + } + other => { + return Err(vm.new_runtime_error(format!( + "Unrecognized action ({other}) in warnings.filters:\n {other}" + ))); + } } + // Create Warning instance if message is a string + let warning_instance = if is_warning { + message + } else { + category.as_object().call((text.clone(),), vm)? + }; + call_show_warning( - // t_state, category, - message, + text, + warning_instance, filename, - lineno, // lineno_obj, + lineno, source_line, source, vm, ) } +#[allow(clippy::too_many_arguments)] fn call_show_warning( category: PyTypeRef, - message: PyStrRef, + text: PyStrRef, + warning_instance: PyObjectRef, filename: PyStrRef, lineno: usize, source_line: Option, @@ -319,20 +487,18 @@ fn call_show_warning( let Some(show_fn) = get_warnings_attr(vm, identifier!(&vm.ctx, _showwarnmsg), source.is_some())? else { - return show_warning(filename, lineno, message, category, source_line, vm); + return show_warning(filename, lineno, text, category, source_line, vm); }; if !show_fn.is_callable() { - return Err(vm.new_type_error("warnings._showwarnmsg() must be set to a callable")); + return Err( + vm.new_type_error("warnings._showwarnmsg() must be set to a callable".to_owned()) + ); } let Some(warnmsg_cls) = get_warnings_attr(vm, identifier!(&vm.ctx, WarningMessage), false)? else { - return Err(vm.new_type_error("unable to get warnings.WarningMessage")); + return Err(vm.new_type_error("unable to get warnings.WarningMessage".to_owned())); }; - // Create a Warning instance by calling category(message) - // This is what warnings module does - let warning_instance = category.as_object().call((message,), vm)?; - let msg = warnmsg_cls.call( vec![ warning_instance, @@ -362,10 +528,41 @@ fn show_warning( Ok(()) } +/// Check if a frame's filename starts with any of the given prefixes. +fn is_filename_to_skip(frame: &crate::frame::Frame, prefixes: &PyTupleRef) -> bool { + let filename = frame.f_code().co_filename(); + let filename_s = filename.as_str(); + prefixes.iter().any(|prefix| { + prefix + .downcast_ref::() + .is_some_and(|s| filename_s.starts_with(s.as_str())) + }) +} + +/// Like Frame::next_external_frame but also skips frames matching prefixes. +fn next_external_frame_with_skip( + frame: &crate::frame::FrameRef, + skip_file_prefixes: Option<&PyTupleRef>, + vm: &VirtualMachine, +) -> Option { + let mut f = frame.f_back(vm); + loop { + let current: crate::frame::FrameRef = f.take()?; + let should_skip = current.is_internal_frame() + || skip_file_prefixes.is_some_and(|prefixes| is_filename_to_skip(¤t, prefixes)); + if should_skip { + f = current.f_back(vm); + } else { + return Some(current); + } + } +} + /// filename, module, and registry are new refs, globals is borrowed /// Returns `Ok` on success, or `Err` on error (no new refs) fn setup_context( mut stack_level: isize, + skip_file_prefixes: Option<&PyTupleRef>, vm: &VirtualMachine, ) -> PyResult< // filename, lineno, module, registry @@ -397,7 +594,7 @@ fn setup_context( break; } if let Some(tmp) = f { - f = tmp.next_external_frame(vm); + f = next_external_frame_with_skip(&tmp, skip_file_prefixes, vm); } else { break; } @@ -417,7 +614,7 @@ fn setup_context( .get_attr(identifier!(vm, __dict__), vm) .and_then(|d| { d.downcast::() - .map_err(|_| vm.new_type_error("sys.__dict__ is not a dictionary")) + .map_err(|_| vm.new_type_error("sys.__dict__ is not a dictionary".to_owned())) })?; (globals, vm.ctx.intern_str(""), 0) }; diff --git a/src/settings.rs b/src/settings.rs index 1847e22c2d4..e1a14e8a2e0 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -355,6 +355,17 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }; settings.warnoptions.push(warn.to_owned()); } + if let Some(val) = get_env("PYTHONWARNINGS") + && let Some(val_str) = val.to_str() + && !val_str.is_empty() + { + for warning in val_str.split(',') { + let warning = warning.trim(); + if !warning.is_empty() { + settings.warnoptions.push(warning.to_owned()); + } + } + } settings.warnoptions.extend(args.warning_control); settings.hash_seed = match (!args.random_hash_seed)