Implement more warnings

This commit is contained in:
Jeong, YunWon committed 2026-02-05 23:50:08 +09:00
1 parent 7271f56c28
commit 654a3eab8c
13 files changed
+570 -220

No files matched your search

+27 -26
View File
@@ -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
-1
View File
@@ -591,7 +591,6 @@ class ImportSideEffectTests(unittest.TestCase):
class StartupImportTests(unittest.TestCase):
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.requires_subprocess()
def test_startup_imports(self):
# Get sys.path in isolated mode (python3 -I)
-26
View File
@@ -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 @@ class _WarningsTests(BaseTest, unittest.TestCase):
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 @@ class EnvironmentVariableTests(BaseTest):
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 @@ class EnvironmentVariableTests(BaseTest):
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 @@ class EnvironmentVariableTests(BaseTest):
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 @@ class EnvironmentVariableTests(BaseTest):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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 @@ class DeprecatedTests(PyPublicAPITests):
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():
+5 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
+5 -3
View File
@@ -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,
+7 -12
View File
@@ -328,19 +328,14 @@ impl Py<Frame> {
}
pub fn next_external_frame(&self, vm: &VirtualMachine) -> Option<FrameRef> {
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
}
}
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+176 -10
View File
@@ -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<PyTypeRef>,
category: OptionalArg<PyObjectRef>,
#[pyarg(any, optional)]
stacklevel: OptionalArg<u32>,
stacklevel: OptionalArg<i32>,
#[pyarg(named, optional)]
source: OptionalArg<PyObjectRef>,
#[pyarg(named, optional)]
skip_file_prefixes: OptionalArg<PyTupleRef>,
}
/// Validate and resolve the category argument, matching get_category() in C.
fn get_category(
message: &PyObjectRef,
category: Option<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<Option<PyTypeRef>> {
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,
args.category.into_option(),
level as isize,
None,
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<PyObjectRef>,
#[pyarg(any, optional)]
registry: OptionalArg<PyObjectRef>,
#[pyarg(any, optional)]
module_globals: OptionalArg<PyObjectRef>,
#[pyarg(named, optional)]
source: OptionalArg<PyObjectRef>,
}
#[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.filename,
args.lineno,
module,
registry,
None, // source_line
args.source.into_option(),
vm,
)
}
+3
View File
@@ -244,7 +244,10 @@ declare_const_name! {
// common names
_attributes,
_fields,
_defaultaction,
_onceregistry,
_showwarnmsg,
filters,
backslashreplace,
close,
copy,
+332 -135
View File
@@ -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<PyObjectRef>,
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<bool> {
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<PyListRef> {
if let Ok(Some(filters_obj)) = get_warnings_attr(vm, identifier!(&vm.ctx, filters), false)
&& let Ok(filters) = filters_obj.try_into_value::<PyListRef>(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<PyObjectRef> {
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<PyObjectRef> {
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<PyTypeRef>,
stack_level: isize,
source: Option<PyObjectRef>,
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<PyTypeRef>,
mut stack_level: isize,
source: Option<PyObjectRef>,
skip_file_prefixes: Option<PyTupleRef>,
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<PyObjectRef> {
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 = get_warnings_filters(vm)?;
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")))?;
// 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<bool> {
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();
if vm.is_none(registry) {
return Ok(false);
}
match version_obj {
Some(version_obj)
if version_obj.try_int(vm).is_ok() || version_obj.is(&filters_version) =>
let current_version = vm.state.warnings.filters_version.load(Ordering::SeqCst);
let version_obj = registry.get_item(identifier!(&vm.ctx, version), vm).ok();
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<PyStr>, vm: &VirtualMachine) -> Option<PyObjectRef> {
@@ -218,10 +331,11 @@ fn normalize_module(filename: &Py<PyStr>, vm: &VirtualMachine) -> Option<PyObjec
Some(obj)
}
/// Core warning logic. message can be either a string or a Warning instance.
#[allow(clippy::too_many_arguments)]
fn warn_explicit(
pub(crate) fn warn_explicit(
category: Option<PyTypeRef>,
message: PyStrRef,
message: PyObjectRef,
filename: PyStrRef,
lineno: usize,
module: Option<PyObjectRef>,
@@ -230,9 +344,29 @@ fn warn_explicit(
source: Option<PyObjectRef>,
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(&registry) && already_warned(&registry, 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()))?;
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(&reg, once_key, true, vm)? {
return Ok(()); // already warned once
}
}
"always" | "all" => { /* fall through to show warning */ }
"module" => {
if !vm.is_none(&registry) {
// Record with the full key (text, category, lineno)
already_warned(&registry, 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(&registry, alt_key, true, vm)? {
return Ok(());
}
}
}
"default" => {
if !vm.is_none(&registry) && already_warned(&registry, key, true, vm)? {
return Ok(());
}
}
other => {
return Err(vm.new_runtime_error(format!(
"Unrecognized action ({other}) in warnings.filters:\n {other}"
)));
}
}
if action.str_utf8(vm)?.as_str().eq("ignore") {
return Ok(());
}
// 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<PyObjectRef>,
@@ -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::<PyStr>()
.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<crate::frame::FrameRef> {
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(&current, 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::<crate::builtins::PyDict>()
.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("<sys>"), 0)
};
+11
View File
@@ -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)