diff --git a/Lib/_py_warnings.py b/Lib/_py_warnings.py index 55f8c0695..ab09913de 100644 --- a/Lib/_py_warnings.py +++ b/Lib/_py_warnings.py @@ -369,9 +369,15 @@ def _setoption(arg): if message or module: import re if message: - message = re.escape(message) + if len(message) >= 2 and message[0] == message[-1] == '/': + message = message[1:-1] + else: + message = re.escape(message) if module: - module = re.escape(module) + r'\z' + if len(module) >= 2 and module[0] == module[-1] == '/': + module = module[1:-1] + else: + module = re.escape(module) + r'\z' if lineno: try: lineno = int(lineno) @@ -381,7 +387,23 @@ def _setoption(arg): raise _wm._OptionError("invalid lineno %r" % (lineno,)) from None else: lineno = 0 - _wm.filterwarnings(action, message, category, module, lineno) + try: + _wm.filterwarnings(action, message, category, module, lineno) + except re.PatternError if message or module else (): + if message: + try: + re.compile(message) + except re.PatternError: + raise _wm._OptionError(f"invalid regular expression for " + f"message: {message!r}") from None + if module: + try: + re.compile(module) + except re.PatternError: + raise _wm._OptionError(f"invalid regular expression for " + f"module: {module!r}") from None + # Should never happen. + raise # Helper for _setoption() @@ -449,9 +471,12 @@ def warn(message, category=None, stacklevel=1, source=None, # Check category argument if category is None: category = UserWarning - if not (isinstance(category, type) and issubclass(category, Warning)): - raise TypeError("category must be a Warning subclass, " - "not '{:s}'".format(type(category).__name__)) + elif not isinstance(category, type): + raise TypeError(f"category must be a Warning subclass, not " + f"'{type(category).__name__}'") + elif not issubclass(category, Warning): + raise TypeError(f"category must be a Warning subclass, not " + f"class '{category.__name__}'") if not isinstance(skip_file_prefixes, tuple): # The C version demands a tuple for implementation performance. raise TypeError('skip_file_prefixes must be a tuple of strs.') @@ -495,14 +520,43 @@ def warn(message, category=None, stacklevel=1, source=None, ) +def _match_filename(pattern, filename, *, MS_WINDOWS=(sys.platform == 'win32')): + if not filename: + return pattern.match('') is not None + if filename[0] == '<' and filename[-1] == '>': + return pattern.match(filename) is not None + + is_py = (filename[-3:].lower() == '.py' + if MS_WINDOWS else + filename.endswith('.py')) + if is_py: + filename = filename[:-3] + if pattern.match(filename): # for backward compatibility + return True + if MS_WINDOWS: + if not is_py and filename[-4:].lower() == '.pyw': + filename = filename[:-4] + is_py = True + if is_py and filename[-9:].lower() in (r'\__init__', '/__init__'): + filename = filename[:-9] + filename = filename.replace('\\', '/') + else: + if is_py and filename.endswith('/__init__'): + filename = filename[:-9] + filename = filename.replace('/', '.') + i = 0 + while True: + if pattern.match(filename, i): + return True + i = filename.find('.', i) + 1 + if not i: + return False + + def warn_explicit(message, category, filename, lineno, module=None, registry=None, module_globals=None, source=None): lineno = int(lineno) - if module is None: - module = filename or "" - if module[-3:].lower() == ".py": - module = module[:-3] # XXX What about leading pathname? if isinstance(message, Warning): text = str(message) category = message.__class__ @@ -524,9 +578,11 @@ def warn_explicit(message, category, filename, lineno, action, msg, cat, mod, ln = item if ((msg is None or msg.match(text)) and issubclass(category, cat) and - (mod is None or mod.match(module)) and - (ln == 0 or lineno == ln)): - break + (ln == 0 or lineno == ln) and + (mod is None or (_match_filename(mod, filename) + if module is None else + mod.match(module)))): + break else: action = _wm.defaultaction # Early exit actions @@ -564,17 +620,18 @@ def warn_explicit(message, category, filename, lineno, linecache.getlines(filename, module_globals) # Print message and context - msg = _wm.WarningMessage(message, category, filename, lineno, source=source) + msg = _wm.WarningMessage(message, category, filename, lineno, + module=module, source=source) _wm._showwarnmsg(msg) class WarningMessage(object): _WARNING_DETAILS = ("message", "category", "filename", "lineno", "file", - "line", "source") + "line", "source", "module") def __init__(self, message, category, filename, lineno, file=None, - line=None, source=None): + line=None, source=None, module=None): self.message = message self.category = category self.filename = filename @@ -582,12 +639,14 @@ class WarningMessage(object): self.file = file self.line = line self.source = source + self.module = module self._category_name = category.__name__ if category else None def __str__(self): - return ("{message : %r, category : %r, filename : %r, lineno : %s, " - "line : %r}" % (self.message, self._category_name, - self.filename, self.lineno, self.line)) + return ("{message : %r, category : %r, module : %r, " + "filename : %r, lineno : %s, line : %r}" % ( + self.message, self._category_name, self.module, + self.filename, self.lineno, self.line)) def __repr__(self): return f'<{type(self).__qualname__} {self}>' @@ -647,8 +706,8 @@ class catch_warnings(object): context = None self._filters = self._module.filters self._module.filters = self._filters[:] - self._showwarning = self._module.showwarning self._showwarnmsg_impl = self._module._showwarnmsg_impl + self._showwarning = self._module.showwarning self._module._filters_mutated_lock_held() if self._record: if _use_context: @@ -656,9 +715,9 @@ class catch_warnings(object): else: log = [] self._module._showwarnmsg_impl = log.append - # Reset showwarning() to the default implementation to make sure - # that _showwarnmsg() calls _showwarnmsg_impl() - self._module.showwarning = self._module._showwarning_orig + # Reset showwarning() to the default implementation to make sure + # that _showwarnmsg() calls _showwarnmsg_impl() + self._module.showwarning = self._module._showwarning_orig else: log = None if self._filter is not None: @@ -673,8 +732,8 @@ class catch_warnings(object): self._module._warnings_context.set(self._saved_context) else: self._module.filters = self._filters - self._module.showwarning = self._showwarning self._module._showwarnmsg_impl = self._showwarnmsg_impl + self._module.showwarning = self._showwarning self._module._filters_mutated_lock_held() diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index 9ee1c5dfe..5f8c2cf00 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -2251,14 +2251,17 @@ impl Blocks { AnyInstruction::Real(Instruction::CallIntrinsic1 { func }) => { match func.get(inst.arg) { IntrinsicFunction1::ListToTuple => { - if matches!(nextop, Some(Instruction::GetIter { .. })) { + fold_constant_sequence_into_load_const( + metadata, + &mut self[block_idx], + i, + )?; + if matches!( + self[block_idx].instructions[i].instr.real(), + Some(Instruction::CallIntrinsic1 { .. }) + ) && matches!(nextop, Some(Instruction::GetIter { .. })) + { self[block_idx].instructions[i].set_to_nop(); - } else { - fold_constant_intrinsic_list_to_tuple( - metadata, - &mut self[block_idx], - i, - )?; } } IntrinsicFunction1::UnaryPositive => { @@ -2267,6 +2270,16 @@ impl Blocks { _ => {} } } + AnyInstruction::Real( + Instruction::ListAppend { .. } | Instruction::SetAdd { .. }, + ) if u32::from(inst.arg) == 1 + && matches!( + nextop, + Some(Instruction::GetIter { .. } | Instruction::ContainsOp { .. }) + ) => + { + fold_constant_sequence_into_load_const(metadata, &mut self[block_idx], i)?; + } AnyInstruction::Real(Instruction::BinaryOp { .. }) => { fold_const_binop(metadata, &mut self[block_idx], i)?; } @@ -5456,20 +5469,24 @@ fn fold_tuple_of_constants( Ok(true) } -fn fold_constant_intrinsic_list_to_tuple( +fn fold_constant_sequence_into_load_const( metadata: &mut CodeUnitMetadata, block: &mut Block, i: usize, ) -> crate::InternalResult { - let Some(Instruction::CallIntrinsic1 { func }) = block.instructions[i].instr.real() else { - return Ok(false); + let (intrinsic, is_set) = match block.instructions[i].instr.real() { + Some(Instruction::CallIntrinsic1 { func }) + if func.get(block.instructions[i].arg) == IntrinsicFunction1::ListToTuple => + { + (true, false) + } + Some(Instruction::ListAppend { .. }) => (false, false), + Some(Instruction::SetAdd { .. }) => (false, true), + _ => return Ok(false), }; - if func.get(block.instructions[i].arg) != IntrinsicFunction1::ListToTuple { - return Ok(false); - } let mut consts_found = 0usize; - let mut expect_append = true; + let mut expect_append = intrinsic; let mut pos = i; while let Some(prev) = pos.checked_sub(1) { pos = prev; @@ -5478,9 +5495,12 @@ fn fold_constant_intrinsic_list_to_tuple( continue; } - if matches!(instr.instr.real(), Some(Instruction::BuildList { .. })) - && u32::from(instr.arg) == 0 - { + let is_start = if is_set { + matches!(instr.instr.real(), Some(Instruction::BuildSet { .. })) + } else { + matches!(instr.instr.real(), Some(Instruction::BuildList { .. })) + }; + if is_start && u32::from(instr.arg) == 0 { if !expect_append { return Ok(false); } @@ -5489,7 +5509,8 @@ fn fold_constant_intrinsic_list_to_tuple( elements .try_reserve_exact(consts_found) .map_err(|_| InternalError::MalformedControlFlowGraph)?; - for idx in (pos..i).rev() { + let end = if intrinsic { i } else { i + 1 }; + for idx in (pos..end).rev() { if matches!(block.instructions[idx].instr.real(), Some(Instruction::Nop)) { continue; } @@ -5506,15 +5527,22 @@ fn fold_constant_intrinsic_list_to_tuple( instr_make_load_const( metadata, &mut block.instructions[i], - ConstantData::Tuple { elements }, + if is_set { + ConstantData::Frozenset { elements } + } else { + ConstantData::Tuple { elements } + }, )?; return Ok(true); } if expect_append { - if !matches!(instr.instr.real(), Some(Instruction::ListAppend { .. })) - || u32::from(instr.arg) != 1 - { + let is_append = if is_set { + matches!(instr.instr.real(), Some(Instruction::SetAdd { .. })) + } else { + matches!(instr.instr.real(), Some(Instruction::ListAppend { .. })) + }; + if !is_append || u32::from(instr.arg) != 1 { return Ok(false); } } else { diff --git a/crates/common/src/cformat.rs b/crates/common/src/cformat.rs index de437f2ce..8117cf7b7 100644 --- a/crates/common/src/cformat.rs +++ b/crates/common/src/cformat.rs @@ -20,6 +20,7 @@ pub enum CFormatErrorType { MissingModuloSign, UnsupportedFormatChar(CodePoint), IncompleteFormat, + StrayModulo, WidthTooBig, PrecisionTooBig, // Unimplemented, @@ -39,6 +40,7 @@ impl fmt::Display for CFormatError { match self.typ { CFormatErrorType::UnmatchedKeyParentheses => write!(f, "incomplete format key"), CFormatErrorType::IncompleteFormat => write!(f, "incomplete format"), + CFormatErrorType::StrayModulo => write!(f, "stray % at position {}", self.index), CFormatErrorType::UnsupportedFormatChar(c) => write!( f, "unsupported format character '{}' ({:#x}) at index {}", @@ -832,8 +834,20 @@ impl CFormatStrOrBytes { let spec = CFormatSpecKeyed::parse(iter, context).map_err(|err| CFormatError { - typ: err.0, - index: err.1, + typ: if context == CFormatContext::Str + && err.0 == CFormatErrorType::IncompleteFormat + { + CFormatErrorType::StrayModulo + } else { + err.0 + }, + index: if context == CFormatContext::Str + && err.0 == CFormatErrorType::IncompleteFormat + { + index + } else { + err.1 + }, })?; parts.push((index, CFormatPart::Spec(spec))); @@ -842,8 +856,16 @@ impl CFormatStrOrBytes { } } else { return Err(CFormatError { - typ: CFormatErrorType::IncompleteFormat, - index: index + 1, + typ: if context == CFormatContext::Str { + CFormatErrorType::StrayModulo + } else { + CFormatErrorType::IncompleteFormat + }, + index: if context == CFormatContext::Str { + index + } else { + index + 1 + }, }); } } else { @@ -991,8 +1013,8 @@ mod tests { assert_eq!( "Hello %".parse::(), Err(CFormatError { - typ: CFormatErrorType::IncompleteFormat, - index: 7 + typ: CFormatErrorType::StrayModulo, + index: 6 }) ); } diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index aa7556473..a2e8e0d63 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -427,7 +427,6 @@ pub struct PyCode { #[pymember(name = "co_posonlyargcount", path = "posonlyarg_count")] #[pymember(name = "co_kwonlyargcount", path = "kwonlyarg_count")] #[pymember(name = "co_stacksize", path = "max_stackdepth")] - #[pymember(name = "co_name", path = "obj_name")] #[pymember(name = "co_qualname", path = "qualname")] #[pymember(name = "co_flags", path = "flags")] // Logical code fields are finalized by `new`. Execution may specialize or @@ -436,6 +435,8 @@ pub struct PyCode { pub code: CodeObject, /// Canonical bytes captured before execution can instrument the code. canonical_code: Box<[u8]>, + /// Preserve a public co_name subclass; exact strings use the interned owner. + name_owner: Option, /// The stable Python object exposed by co_code, allocated on first access. code_bytes: OnceCell, /// Slot-indexed names, equivalent to CPython's `co_localsplusnames`. @@ -578,7 +579,7 @@ impl PyCode { CodeField::Names => self.names_tuple.as_object(), CodeField::LocalsPlusNames => self.localsplus_names_tuple.as_object(), CodeField::LocalsPlusKinds => self.localsplus_kinds_bytes.as_object(), - CodeField::Name => self.code.obj_name.as_object(), + CodeField::Name => self.name_object(), CodeField::Qualname => self.code.qualname.as_object(), CodeField::LineTable => self.linetable_bytes.as_object(), CodeField::ExceptionTable => self.exceptiontable_bytes.as_object(), @@ -661,6 +662,7 @@ impl PyCode { cellvars_tuple: OnceCell::new(), freevars_tuple: OnceCell::new(), canonical_code, + name_owner: None, code_bytes: OnceCell::new(), localsplus_names, source_path: AtomicPtr::new(sp), @@ -739,6 +741,18 @@ impl PyCode { self.localsplus_names[index] } + fn name_object(&self) -> &PyObject { + self.name_owner + .as_ref() + .map_or_else(|| self.code.obj_name.as_object(), |name| name.as_object()) + } + + fn retain_name_subclass(&mut self, name: PyStrRef, ctx: &Context) { + if !name.class().is(ctx.types.str_type) { + self.name_owner = Some(name); + } + } + pub fn source_path(&self) -> &'static PyStrInterned { // SAFETY: always points to a valid &'static PyStrInterned (interned strings are never deallocated) unsafe { &*self.source_path.load(Ordering::Relaxed) } @@ -912,7 +926,7 @@ impl Comparable for PyCode { let other = class_or_notimplemented!(Self, other); let a = &zelf.code; let b = &other.code; - let eq = a.obj_name == b.obj_name + let eq = vm.bool_eq(zelf.name_object(), other.name_object())? && a.arg_count == b.arg_count && a.posonlyarg_count == b.posonlyarg_count && a.kwonlyarg_count == b.kwonlyarg_count @@ -951,7 +965,7 @@ impl Hashable for PyCode { let code = &zelf.code; // Hash a tuple of key attributes, matching CPython's code_hash let tuple = vm.ctx.new_tuple(vec![ - vm.ctx.new_str(code.obj_name.as_str()).into(), + zelf.name_object().to_owned(), vm.ctx.new_int(code.arg_count).into(), vm.ctx.new_int(code.posonlyarg_count).into(), vm.ctx.new_int(code.kwonlyarg_count).into(), @@ -1127,6 +1141,7 @@ impl Constructor for PyCode { }; let mut result = Self::new(code, &vm.ctx); + result.retain_name_subclass(args.name, &vm.ctx); result.linetable_bytes = args.linetable; result.exceptiontable_bytes = args.exceptiontable; Ok(result) @@ -1160,6 +1175,13 @@ impl Py { self.code.first_line_number.map_or(0, |n| n.get() as _) } + #[pygetset] + fn co_name(&self) -> PyStrRef { + self.name_owner + .clone() + .unwrap_or_else(|| self.code.obj_name.to_owned()) + } + #[pygetset] fn co_consts(&self) -> PyTupleRef { self.code @@ -1606,7 +1628,7 @@ impl Py { let obj_name = match co_name { OptionalArg::Present(obj_name) => obj_name, - OptionalArg::Missing => self.code.obj_name.to_owned(), + OptionalArg::Missing => self.co_name(), }; let names = match co_names { @@ -1735,6 +1757,7 @@ impl Py { }; let mut result = PyCode::new(new_code, &vm.ctx); + result.retain_name_subclass(obj_name, &vm.ctx); result.retain_normalized_names(names, None); result.linetable_bytes = linetable; result.exceptiontable_bytes = exceptiontable; diff --git a/crates/vm/src/cformat.rs b/crates/vm/src/cformat.rs index abb845f9d..9a68e7788 100644 --- a/crates/vm/src/cformat.rs +++ b/crates/vm/src/cformat.rs @@ -203,8 +203,14 @@ fn spec_format_string( spec: &CFormatSpec, obj: PyObjectRef, argument: FormatArgument<'_>, + format_start: usize, ) -> PyResult { match &spec.format_type { + CFormatType::Unsupported { ch, .. } if ch.to_char_lossy().is_ascii_alphabetic() => Err(vm + .new_value_error(format!( + "unsupported format %{} at position {format_start}", + ch.to_char_lossy(), + ))), CFormatType::Unsupported { ch, index } => Err(vm.new_value_error(format!( "unsupported format character '{}' ({:#x}) at index {index}", ch.to_char_lossy(), @@ -245,15 +251,27 @@ fn spec_format_string( Ok(spec.format_number(i.as_bigint()).into()) } else { Err(vm.new_type_error(format!( - "%{} format: an integer is required, not {}", + "{}: %{} requires an integer, not {}", + argument.description(vm)?, spec.format_type.to_char(), - obj.class().name() + obj.class().slot_name() ))) } } }, CFormatType::Float(_) => { - let value = ArgIntoFloat::try_from_object(vm, obj)?; + let value = match ArgIntoFloat::try_from_object(vm, obj.clone()) { + Ok(value) => value, + Err(error) if error.fast_isinstance(vm.ctx.exceptions.type_error) => { + return Err(vm.new_type_error(format!( + "{}: %{} requires a real number, not {}", + argument.description(vm)?, + spec.format_type.to_char(), + obj.class().slot_name() + ))); + } + Err(error) => return Err(error), + }; Ok(spec.format_float(value.into()).into()) } CFormatType::Character(CCharacterType::Character) => { @@ -504,7 +522,7 @@ pub(crate) fn cformat_string( } // dict - for (_, part) in format { + for (format_start, part) in format { match part { CFormatPart::Literal(literal) => result.push_wtf8(&literal), CFormatPart::Spec(CFormatSpecKeyed { mapping_key, spec }) => { @@ -516,6 +534,7 @@ pub(crate) fn cformat_string( &spec, value, FormatArgument::Mapping(key.as_object()), + format_start, )?; result.push_wtf8(&part_result); } @@ -540,7 +559,7 @@ pub(crate) fn cformat_string( let mut value_iter = value_iter.inspect(|_| argument_index.set(argument_index.get() + 1)); let is_tuple = values_obj.downcast_ref::().is_some(); - for (_, part) in format { + for (format_start, part) in format { match part { CFormatPart::Literal(literal) => result.push_wtf8(&literal), CFormatPart::Spec(CFormatSpecKeyed { mut spec, .. }) => { @@ -561,7 +580,8 @@ pub(crate) fn cformat_string( } else { FormatArgument::Scalar }; - let part_result = spec_format_string(vm, &spec, value.to_owned(), argument)?; + let part_result = + spec_format_string(vm, &spec, value.to_owned(), argument, format_start)?; result.push_wtf8(&part_result); } } diff --git a/crates/vm/src/warn.rs b/crates/vm/src/warn.rs index 52e440a9d..5b578ca1c 100644 --- a/crates/vm/src/warn.rs +++ b/crates/vm/src/warn.rs @@ -336,22 +336,38 @@ fn update_registry( already_warned(registry, &altkey, true, vm) } -fn normalize_module(filename: &Py, vm: &VirtualMachine) -> PyObjectRef { - match filename.byte_len() { - 0 => vm.new_pyobj(""), - len if len >= 3 && filename.as_bytes().ends_with(b".py") => { - vm.new_pyobj(&filename.as_wtf8()[..len - 3]) - } - _ => filename.as_object().to_owned(), +fn check_module_matched( + pattern: &PyObject, + module: Option<&PyObject>, + filename: &Py, + vm: &VirtualMachine, +) -> PyResult { + if let Some(module) = module { + return check_matched(pattern, module, vm); } + if vm.is_none(pattern) { + return Ok(true); + } + if pattern.class().is(vm.ctx.types.str_type) { + return Ok(false); + } + let warnings = vm.import("_py_warnings", 0)?; + vm.call_method( + &warnings, + "_match_filename", + (pattern.to_owned(), filename.to_owned()), + )? + .is_true(vm) } /// Search a filters list for a matching action. +#[allow(clippy::too_many_arguments)] fn filter_search( category: &PyObject, text: &PyObject, lineno: usize, - module: &PyObject, + module: Option<&PyObject>, + filename: &Py, filters: &PyListRef, list_name: &str, vm: &VirtualMachine, @@ -380,7 +396,7 @@ fn filter_search( } let good_msg = check_matched(&tmp_item.as_slice()[1], text, vm)?; let is_subclass = category.is_subclass(&tmp_item.as_slice()[2], vm)?; - let good_mod = check_matched(&tmp_item.as_slice()[3], module, vm)?; + let good_mod = check_module_matched(&tmp_item.as_slice()[3], module, filename, vm)?; let ln: usize = tmp_item.as_slice()[4] .try_int(vm) .map_or(0, |v| v.as_u32_mask() as _); @@ -397,7 +413,8 @@ fn get_filter( category: &PyObject, text: &PyObject, lineno: usize, - module: &PyObject, + module: Option<&PyObject>, + filename: &Py, vm: &VirtualMachine, ) -> PyResult { if let Some(context_filters) = get_warnings_context_filters(vm)? { @@ -406,6 +423,7 @@ fn get_filter( text, lineno, module, + filename, &context_filters, "_warnings_context _filters", vm, @@ -416,7 +434,9 @@ fn get_filter( } let filters = get_warnings_filters(vm)?; - if let Some(action) = filter_search(category, text, lineno, module, &filters, "filters", vm)? { + if let Some(action) = filter_search( + category, text, lineno, module, filename, &filters, "filters", vm, + )? { return Ok(action); } get_default_action(vm) @@ -504,9 +524,9 @@ pub fn warn_explicit( source: Option, vm: &VirtualMachine, ) -> PyResult<()> { - // Normalize module. None → silent return (late-shutdown safety). - let module = module.unwrap_or_else(|| normalize_module(&filename, vm)); - if vm.is_none(&module) { + // Explicit None remains a late-shutdown silent return. An omitted module + // is matched against the filename by the canonical warnings helper. + if module.as_ref().is_some_and(|module| vm.is_none(module)) { return Ok(()); } @@ -543,7 +563,14 @@ pub fn warn_explicit( } // Get filter action - let action = get_filter(category.as_object(), text.as_object(), lineno, &module, vm)?; + let action = get_filter( + category.as_object(), + text.as_object(), + lineno, + module.as_deref(), + &filename, + vm, + )?; let action_str = PyStrRef::try_from_object(vm, action) .map_err(|_| vm.new_type_error("action must be a string"))?;