diff --git a/vm/src/builtins/dict.rs b/vm/src/builtins/dict.rs index 7f8d26066..cccaed000 100644 --- a/vm/src/builtins/dict.rs +++ b/vm/src/builtins/dict.rs @@ -288,7 +288,7 @@ impl PyDict { } #[pymethod] - fn clear(&self) { + pub fn clear(&self) { self.entries.clear() } diff --git a/vm/src/vm/context.rs b/vm/src/vm/context.rs index 8482e5f9f..cde7271bc 100644 --- a/vm/src/vm/context.rs +++ b/vm/src/vm/context.rs @@ -213,6 +213,7 @@ declare_const_name! { keys, items, values, + version, update, copy, flush, diff --git a/vm/src/vm/mod.rs b/vm/src/vm/mod.rs index 6a37864ec..e8091be1f 100644 --- a/vm/src/vm/mod.rs +++ b/vm/src/vm/mod.rs @@ -31,7 +31,9 @@ use crate::{ import, protocol::PyIterIter, scope::Scope, - signal, stdlib, AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, + signal, stdlib, + warn::WarningsState, + AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, }; use crossbeam_utils::atomic::AtomicCell; use std::sync::atomic::AtomicBool; @@ -88,6 +90,7 @@ pub struct PyGlobalState { pub atexit_funcs: PyMutex>, pub codec_registry: CodecsRegistry, pub finalizing: AtomicBool, + pub warnings: WarningsState, } pub fn process_hash_secret_seed() -> u32 { @@ -136,6 +139,8 @@ impl VirtualMachine { let codec_registry = CodecsRegistry::new(&ctx); + let warnings = WarningsState::init_state(&ctx); + let mut vm = VirtualMachine { builtins, sys_module, @@ -161,6 +166,7 @@ impl VirtualMachine { atexit_funcs: PyMutex::default(), codec_registry, finalizing: AtomicBool::new(false), + warnings, }), initialized: false, recursion_depth: Cell::new(0), diff --git a/vm/src/warn.rs b/vm/src/warn.rs index bb51d54b8..2005419ba 100644 --- a/vm/src/warn.rs +++ b/vm/src/warn.rs @@ -1,8 +1,53 @@ use crate::{ - builtins::{PyDict, PyStrRef, PyType, PyTypeRef}, - AsObject, Py, PyObjectRef, PyResult, VirtualMachine, + builtins::{PyDict, PyDictRef, PyListRef, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef}, + convert::{IntoObject, TryFromObject}, + types::PyComparisonOp, + AsObject, Context, Py, PyObjectRef, PyResult, VirtualMachine, }; +pub struct WarningsState { + filters: PyListRef, + _once_registry: PyDictRef, + default_action: PyStrRef, + filters_version: usize, +} + +impl WarningsState { + fn create_filter(ctx: &Context) -> PyListRef { + ctx.new_list(vec![ctx + .new_tuple(vec![ + 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()]) + } + + pub fn init_state(ctx: &Context) -> WarningsState { + WarningsState { + filters: Self::create_filter(ctx), + _once_registry: PyDict::new_ref(ctx), + default_action: ctx.new_str("default"), + filters_version: 0, + } + } +} + +fn check_matched(obj: &PyObjectRef, arg: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + if obj.class().is(vm.ctx.types.none_type) { + return Ok(true); + } + + if obj.rich_compare_bool(arg, PyComparisonOp::Eq, vm)? { + return Ok(false); + } + + let result = vm.invoke(obj, (arg.to_owned(),)); + Ok(result.is_ok()) +} + pub fn py_warn( category: &Py, message: String, @@ -31,19 +76,164 @@ pub fn warn( ) } +fn get_default_action(vm: &VirtualMachine) -> PyResult { + vm.state + .warnings + .default_action + .clone() + .try_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".to_string()))?; + + /* 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)?; + if tmp_item.len() == 5 { + Some(tmp_item) + } else { + None + } + } else { + None + }; + let tmp_item = tmp_item.ok_or_else(|| { + vm.new_value_error(format!("_warnings.filters item {} isn't a 5-tuple", i)) + })?; + + /* Python code: action, msg, cat, mod, ln = item */ + let action = if let Some(action) = tmp_item.get(0) { + action.str(vm).map(|action| action.into_object()) + } else { + Err(vm.new_type_error("action must be a string".to_string())) + }; + + let good_msg = if let Some(msg) = tmp_item.get(1) { + check_matched(msg, &text, vm)? + } else { + false + }; + + let is_subclass = if let Some(cat) = tmp_item.get(2) { + category.fast_isinstance(&cat.class()) + } else { + false + }; + + let good_mod = if let Some(item_mod) = tmp_item.get(3) { + check_matched(item_mod, &module, vm)? + } else { + false + }; + + let ln = tmp_item.get(4).map_or(0, |ln_obj| { + ln_obj.try_int(vm).map_or(0, |ln| ln.as_u32_mask() as _) + }); + + if good_msg && good_mod && is_subclass && (ln == 0 || lineno == ln) { + _item = tmp_item; + return action; + } + } + + get_default_action(vm) +} + +fn already_warned( + registry: PyObjectRef, + key: PyObjectRef, + should_set: bool, + vm: &VirtualMachine, +) -> PyResult { + 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 already_warned = registry.get_item(key.as_ref(), vm)?; + if already_warned.is_true(vm)? { + return Ok(true); + } + } + _ => { + 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); + } + } + } + } + + /* 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) +} + +fn normalize_module(filename: PyStrRef, vm: &VirtualMachine) -> Option { + let obj = match filename.char_len() { + 0 => vm.new_pyobj(""), + len if len >= 3 && filename.as_str().ends_with(".py") => { + vm.new_pyobj(&filename.as_str()[..len - 3]) + } + _ => filename.as_object().to_owned(), + }; + Some(obj) +} + #[allow(clippy::too_many_arguments)] fn warn_explicit( category: Option, message: PyStrRef, - _filename: PyStrRef, - _lineno: usize, - _module: PyObjectRef, - _registry: PyObjectRef, + filename: PyStrRef, + lineno: usize, + module: Option, + registry: PyObjectRef, _source_line: Option, _source: Option, vm: &VirtualMachine, ) -> PyResult<()> { - // TODO: Implement correctly + let registry: PyObjectRef = registry + .try_into_value(vm) + .map_err(|_| vm.new_type_error("'registry' must be a dict or None".to_owned()))?; + + // Normalize module. + let module = match module.or_else(|| normalize_module(filename, vm)) { + Some(module) => module, + None => return Ok(()), + }; + + // Normalize message. + let text = message.as_str(); + let category = if let Some(category) = category { if !category.fast_issubclass(vm.ctx.exceptions.warning) { return Err(vm.new_type_error(format!( @@ -55,8 +245,49 @@ fn warn_explicit( } else { vm.ctx.exceptions.user_warning.to_owned() }; + + let category = if message.fast_isinstance(vm.ctx.exceptions.warning) { + message.class().into_owned() + } else { + category + }; + + // Create key. + let key = PyTuple::new_ref( + vec![ + vm.ctx.new_int(3).into(), + vm.ctx.new_str(text).into(), + category.as_object().to_owned(), + vm.ctx.new_int(lineno).into(), + ], + &vm.ctx, + ); + + if !vm.is_none(registry.as_object()) && already_warned(registry, key.into_object(), false, vm)? + { + return Ok(()); + } + + let item = vm.ctx.new_tuple(vec![]); + let action = get_filter( + category.as_object().to_owned(), + vm.ctx.new_str(text).into(), + lineno, + module, + item, + vm, + )?; + + if action.str(vm)?.as_str().eq("error") { + return Err(vm.new_type_error(message.to_string())); + } + + if action.str(vm)?.as_str().eq("ignore") { + return Ok(()); + } + let stderr = crate::stdlib::sys::PyStderr(vm); - writeln!(stderr, "{}: {}", category.name(), message.as_str(),); + writeln!(stderr, "{}: {}", category.name(), text,); Ok(()) } @@ -67,7 +298,7 @@ fn setup_context( vm: &VirtualMachine, ) -> PyResult< // filename, lineno, module, registry - (PyStrRef, usize, PyObjectRef, PyObjectRef), + (PyStrRef, usize, Option, PyObjectRef), > { let __warningregistry__ = "__warningregistry__"; let __name__ = "__name__"; @@ -120,5 +351,5 @@ fn setup_context( let module = globals .get_item(__name__, vm) .unwrap_or_else(|_| vm.new_pyobj("")); - Ok((filename.to_owned(), lineno, module, registry)) + Ok((filename.to_owned(), lineno, Some(module), registry)) }