From 63393906291a1d0f701a8a3e63a85c25cda22e2f Mon Sep 17 00:00:00 2001 From: snowapril Date: Sun, 10 Oct 2021 23:47:32 +0900 Subject: [PATCH 1/7] Implement keyword suggestion routine `suggestions.rs` is almost porting of implementation of [this](https://github.com/python/cpython/pull/16856) and [this](https://github.com/python/cpython/pull/25397). Signed-off-by: snowapril --- common/src/str.rs | 102 ++++++++++++++++++++++++++++++++++++++++ vm/src/builtins/code.rs | 2 +- vm/src/lib.rs | 1 + vm/src/suggestion.rs | 75 +++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 vm/src/suggestion.rs diff --git a/common/src/str.rs b/common/src/str.rs index de2e68168..5bba0a557 100644 --- a/common/src/str.rs +++ b/common/src/str.rs @@ -98,6 +98,108 @@ pub const fn bytes_is_ascii(x: &str) -> bool { true } +pub mod levenshtein { + use std::cell::RefCell; + use std::thread_local; + + pub const MOVE_COST: usize = 2; + const CASE_COST: usize = 1; + const MAX_STRING_SIZE: usize = 40; + + fn substitution_cost(mut a: u8, mut b: u8) -> usize { + if (a & 31) != (b & 31) { + return MOVE_COST; + } + if a == b { + return 0; + } + if (b'A'..=b'Z').contains(&a) { + a += b'a' - b'A'; + } + if (b'A'..=b'Z').contains(&b) { + b += b'a' - b'A'; + } + if a == b { + CASE_COST + } else { + MOVE_COST + } + } + + pub fn levenshtein_distance(a: &str, b: &str, max_cost: usize) -> usize { + thread_local! { + static BUFFER: RefCell<[usize; MAX_STRING_SIZE]> = RefCell::new([0usize; MAX_STRING_SIZE]); + } + + if a == b { + return 0; + } + + let (mut a_bytes, mut b_bytes) = (a.as_bytes(), b.as_bytes()); + let (mut a_begin, mut a_end) = (0usize, a.len()); + let (mut b_begin, mut b_end) = (0usize, b.len()); + + while a_end > 0 && b_end > 0 && (a_bytes[a_begin] == b_bytes[b_begin]) { + a_begin += 1; + b_begin += 1; + a_end -= 1; + b_end -= 1; + } + while a_end > 0 + && b_end > 0 + && (a_bytes[a_begin + a_end - 1] == b_bytes[b_begin + b_end - 1]) + { + a_end -= 1; + b_end -= 1; + } + if a_end == 0 || b_end == 0 { + return (a_end + b_end) * MOVE_COST; + } + if a_end > MAX_STRING_SIZE || b_end > MAX_STRING_SIZE { + return max_cost + 1; + } + + if b_end < a_end { + std::mem::swap(&mut a_bytes, &mut b_bytes); + std::mem::swap(&mut a_begin, &mut b_begin); + std::mem::swap(&mut a_end, &mut b_end); + } + + if (b_end - a_end) * MOVE_COST > max_cost { + return max_cost + 1; + } + + BUFFER.with(|buffer| { + let mut buffer = buffer.borrow_mut(); + for i in 0..a_end { + buffer[i] = (i + 1) * MOVE_COST; + } + + let mut result = 0usize; + for (b_index, b_code) in b_bytes[b_begin..(b_begin + b_end)].iter().enumerate() { + result = b_index * MOVE_COST; + let mut distance = result; + let mut minimum = usize::MAX; + for (a_index, a_code) in a_bytes[a_begin..(a_begin + a_end)].iter().enumerate() { + let substitute = distance + substitution_cost(*b_code, *a_code); + distance = buffer[a_index]; + let insert_delete = usize::min(result, distance) + MOVE_COST; + result = usize::min(insert_delete, substitute); + + buffer[a_index] = result; + if result < minimum { + minimum = result; + } + } + if minimum > max_cost { + return max_cost + 1; + } + } + result + }) + } +} + #[macro_export] macro_rules! ascii { ($x:literal) => {{ diff --git a/vm/src/builtins/code.rs b/vm/src/builtins/code.rs index 1672f36c8..05ebe7d0d 100644 --- a/vm/src/builtins/code.rs +++ b/vm/src/builtins/code.rs @@ -256,7 +256,7 @@ impl PyRef { } #[pyproperty] - fn co_varnames(self, vm: &VirtualMachine) -> PyTupleRef { + pub fn co_varnames(self, vm: &VirtualMachine) -> PyTupleRef { let varnames = self .code .varnames diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 9d91fc14d..2f3624c2a 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -70,6 +70,7 @@ mod sequence; mod signal; pub mod sliceable; pub mod stdlib; +pub mod suggestion; pub mod types; pub mod utils; pub mod version; diff --git a/vm/src/suggestion.rs b/vm/src/suggestion.rs new file mode 100644 index 000000000..206d4d94d --- /dev/null +++ b/vm/src/suggestion.rs @@ -0,0 +1,75 @@ +use crate::{ + builtins::{PyStr, PyStrRef}, + exceptions::types::PyBaseExceptionRef, + sliceable::PySliceableSequence, + IdProtocol, PyObjectRef, TypeProtocol, VirtualMachine, +}; +use rustpython_common::str::levenshtein::{levenshtein_distance, MOVE_COST}; +use std::iter::ExactSizeIterator; + +const MAX_CANDIDATE_ITEMS: usize = 750; + +fn calculate_suggestions<'a>( + dir_iter: impl ExactSizeIterator, + name: &PyObjectRef, +) -> Option { + if dir_iter.len() >= MAX_CANDIDATE_ITEMS { + return None; + } + + let mut suggestion: Option<&PyStrRef> = None; + let mut suggestion_distance = usize::MAX; + let name = name.downcast_ref::()?; + + for item in dir_iter { + let item_name = item.downcast_ref::()?; + if name.as_str() == item_name.as_str() { + continue; + } + // No more than 1/3 of the characters should need changed + let max_distance = usize::min( + (name.len() + item_name.len() + 3) * MOVE_COST / 6, + suggestion_distance - 1, + ); + let current_distance = + levenshtein_distance(name.as_str(), item_name.as_str(), max_distance); + if current_distance > max_distance { + continue; + } + if suggestion.is_none() || current_distance < suggestion_distance { + suggestion = Some(item_name); + suggestion_distance = current_distance; + } + } + suggestion.cloned() +} + +pub fn offer_suggestions(exc: &PyBaseExceptionRef, vm: &VirtualMachine) -> Option { + if exc.class().is(&vm.ctx.exceptions.attribute_error) { + let name = exc.as_object().clone().get_attr("name", vm).unwrap(); + let obj = exc.as_object().clone().get_attr("obj", vm).unwrap(); + + calculate_suggestions(vm.dir(Some(obj)).ok()?.borrow_vec().iter(), &name) + } else if exc.class().is(&vm.ctx.exceptions.name_error) { + let name = exc.as_object().clone().get_attr("name", vm).unwrap(); + let mut tb = exc.traceback().unwrap(); + while let Some(traceback) = tb.next.clone() { + tb = traceback; + } + + let varnames = tb.frame.code.clone().co_varnames(vm); + if let Some(suggestions) = calculate_suggestions(varnames.as_slice().iter(), &name) { + return Some(suggestions); + }; + + let globals = vm.extract_elements(tb.frame.globals.as_object()).ok()?; + if let Some(suggestions) = calculate_suggestions(globals.as_slice().iter(), &name) { + return Some(suggestions); + }; + + let builtins = vm.extract_elements(tb.frame.builtins.as_object()).ok()?; + calculate_suggestions(builtins.as_slice().iter(), &name) + } else { + None + } +} From db0d9df53f3448fa9c00a6310921e4ebbc44c749 Mon Sep 17 00:00:00 2001 From: snowapril Date: Sun, 10 Oct 2021 23:49:47 +0900 Subject: [PATCH 2/7] Add `offer_suggestions` on `write_exception_inner` Signed-off-by: snowapril --- vm/src/exceptions.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 53e733748..37f86a55d 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -7,6 +7,7 @@ use crate::{ function::{ArgIterable, FuncArgs, IntoPyException, IntoPyObject}, py_io::{self, Write}, stdlib::sys, + suggestion::offer_suggestions, IdProtocol, PyClassImpl, PyContext, PyObjectRef, PyRef, PyResult, PyValue, StaticType, TryFromObject, TypeProtocol, VirtualMachine, }; @@ -129,17 +130,22 @@ impl VirtualMachine { let varargs = exc.args(); let args_repr = vm.exception_args_as_string(varargs, true); - let exc_type = exc.class(); - let exc_name = exc_type.name(); + let exc_class = exc.class(); + let exc_name = exc_class.name(); match args_repr.len() { - 0 => writeln!(output, "{}", exc_name), - 1 => writeln!(output, "{}: {}", exc_name, args_repr[0]), - _ => writeln!( + 0 => write!(output, "{}", exc_name), + 1 => write!(output, "{}: {}", exc_name, args_repr[0]), + _ => write!( output, "{}: ({})", exc_name, args_repr.into_iter().format(", ") ), + }?; + + match offer_suggestions(exc, vm) { + Some(suggestions) => writeln!(output, ". Did you mean: '{}'?", suggestions.to_string()), + None => writeln!(output), } } From ff54d8ae5398bce38fc705e48c20650d19d22140 Mon Sep 17 00:00:00 2001 From: snowapril Date: Sun, 10 Oct 2021 23:50:56 +0900 Subject: [PATCH 3/7] Add `name` and `obj` attributes to `PyAttributeError` Signed-off-by: snowapril --- vm/src/exceptions.rs | 5 ++++- vm/src/protocol/object.rs | 5 ++++- vm/src/pyobject.rs | 6 ++++-- vm/src/vm.rs | 13 +++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 37f86a55d..94b464548 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -702,7 +702,10 @@ impl ExceptionZoo { extend_exception!(PyZeroDivisionError, ctx, &excs.zero_division_error); extend_exception!(PyAssertionError, ctx, &excs.assertion_error); - extend_exception!(PyAttributeError, ctx, &excs.attribute_error); + extend_exception!(PyAttributeError, ctx, &excs.attribute_error, { + "name" => ctx.none(), + "obj" => ctx.none(), + }); extend_exception!(PyBufferError, ctx, &excs.buffer_error); extend_exception!(PyEOFError, ctx, &excs.eof_error); diff --git a/vm/src/protocol/object.rs b/vm/src/protocol/object.rs index 7bcd081ca..6b0580cd1 100644 --- a/vm/src/protocol/object.rs +++ b/vm/src/protocol/object.rs @@ -32,7 +32,10 @@ impl PyObjectRef { .class() .mro_find_map(|cls| cls.slots.getattro.load()) .unwrap(); - getattro(self, attr_name, vm) + getattro(self.clone(), attr_name.clone(), vm).map_err(|exc| { + vm.set_attribute_error_context(&exc, self, attr_name); + exc + }) } pub fn call_set_attr( diff --git a/vm/src/pyobject.rs b/vm/src/pyobject.rs index 12b182dcc..d6dcf0880 100644 --- a/vm/src/pyobject.rs +++ b/vm/src/pyobject.rs @@ -1103,11 +1103,13 @@ impl PyMethod { drop(cls); vm.invoke(&getter, (obj, name)).map(Self::Attribute) } else { - Err(vm.new_attribute_error(format!( + let exc = vm.new_attribute_error(format!( "'{}' object has no attribute '{}'", cls.name(), name - ))) + )); + vm.set_attribute_error_context(&exc, obj.clone(), name); + Err(exc) } } diff --git a/vm/src/vm.rs b/vm/src/vm.rs index 6f11bea8f..219492fbc 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -1413,6 +1413,19 @@ impl VirtualMachine { } } + pub fn set_attribute_error_context( + &self, + exc: &PyBaseExceptionRef, + obj: PyObjectRef, + name: PyStrRef, + ) { + if exc.class().is(&self.ctx.exceptions.attribute_error) { + let exc = exc.as_object(); + exc.set_attr("name", name, self).unwrap(); + exc.set_attr("obj", obj, self).unwrap(); + } + } + // get_method should be used for internal access to magic methods (by-passing // the full getattribute look-up. pub fn get_method_or_type_error( From 1ee74403e229b3587691fcbeb49353439bdceb05 Mon Sep 17 00:00:00 2001 From: snowapril Date: Sun, 10 Oct 2021 23:51:35 +0900 Subject: [PATCH 4/7] Add `name` field on `PyNameError` Signed-off-by: snowapril --- vm/src/exceptions.rs | 4 +++- vm/src/frame.rs | 21 ++++++++++++++------- vm/src/vm.rs | 11 ++++++++--- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 94b464548..fd0264036 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -721,7 +721,9 @@ impl ExceptionZoo { }); extend_exception!(PyMemoryError, ctx, &excs.memory_error); - extend_exception!(PyNameError, ctx, &excs.name_error); + extend_exception!(PyNameError, ctx, &excs.name_error, { + "name" => ctx.none(), + }); extend_exception!(PyUnboundLocalError, ctx, &excs.unbound_local_error); // os errors: diff --git a/vm/src/frame.rs b/vm/src/frame.rs index 5084c2a0b..fe99c452d 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -448,10 +448,13 @@ impl ExecutingFrame<'_> { ) } else { let name = &self.code.freevars[i - self.code.cellvars.len()]; - vm.new_name_error(format!( - "free variable '{}' referenced before assignment in enclosing scope", - name - )) + vm.new_name_error( + format!( + "free variable '{}' referenced before assignment in enclosing scope", + name + ), + name, + ) } } @@ -609,7 +612,9 @@ impl ExecutingFrame<'_> { match res { Ok(()) => {} Err(e) if e.isinstance(&vm.ctx.exceptions.key_error) => { - return Err(vm.new_name_error(format!("name '{}' is not defined", name))) + return Err( + vm.new_name_error(format!("name '{}' is not defined", name), name) + ) } Err(e) => return Err(e), } @@ -620,7 +625,9 @@ impl ExecutingFrame<'_> { match self.globals.del_item(name.clone(), vm) { Ok(()) => {} Err(e) if e.isinstance(&vm.ctx.exceptions.key_error) => { - return Err(vm.new_name_error(format!("name '{}' is not defined", name))) + return Err( + vm.new_name_error(format!("name '{}' is not defined", name), name) + ) } Err(e) => return Err(e), } @@ -1124,7 +1131,7 @@ impl ExecutingFrame<'_> { fn load_global_or_builtin(&self, name: &PyStrRef, vm: &VirtualMachine) -> PyResult { self.globals .get_chain(self.builtins, name.clone(), vm)? - .ok_or_else(|| vm.new_name_error(format!("name '{}' is not defined", name))) + .ok_or_else(|| vm.new_name_error(format!("name '{}' is not defined", name), name)) } #[cfg_attr(feature = "flame-it", flame("Frame"))] diff --git a/vm/src/vm.rs b/vm/src/vm.rs index 219492fbc..fccf0b72c 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -666,9 +666,14 @@ impl VirtualMachine { self.new_exception_msg(type_error, msg) } - pub fn new_name_error(&self, msg: String) -> PyBaseExceptionRef { - let name_error = self.ctx.exceptions.name_error.clone(); - self.new_exception_msg(name_error, msg) + pub fn new_name_error(&self, msg: String, name: &PyStrRef) -> PyBaseExceptionRef { + let name_error_type = self.ctx.exceptions.name_error.clone(); + let name_error = self.new_exception_msg(name_error_type, msg); + name_error + .as_object() + .set_attr("name", name.clone(), self) + .unwrap(); + name_error } pub fn new_unsupported_unary_error(&self, a: &PyObjectRef, op: &str) -> PyBaseExceptionRef { From 8d0cefeb643fea5e9da9064746e0dd0bc7469c27 Mon Sep 17 00:00:00 2001 From: snowapril Date: Mon, 11 Oct 2021 11:09:59 +0900 Subject: [PATCH 5/7] update `test_exceptions.py` from cpython 3.10 for suggestion feature Signed-off-by: snowapril --- Lib/test/test_exceptions.py | 527 ++++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index d7442a7b0..5d9d36589 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -12,6 +12,7 @@ from test.support import (TESTFN, captured_stderr, check_impl_detail, check_warnings, cpython_only, gc_collect, run_unittest, no_tracing, unlink, import_module, script_helper, SuppressCrashReport) +from test import support class NaiveException(Exception): def __init__(self, x): self.x = x @@ -1327,6 +1328,532 @@ class ExceptionTests(unittest.TestCase): next(i) next(i) +global_for_suggestions = None + +class NameErrorTests(unittest.TestCase): + def test_name_error_has_name(self): + try: + bluch + except NameError as exc: + self.assertEqual("bluch", exc.name) + + def test_name_error_suggestions(self): + def Substitution(): + noise = more_noise = a = bc = None + blech = None + print(bluch) + + def Elimination(): + noise = more_noise = a = bc = None + blch = None + print(bluch) + + def Addition(): + noise = more_noise = a = bc = None + bluchin = None + print(bluch) + + def SubstitutionOverElimination(): + blach = None + bluc = None + print(bluch) + + def SubstitutionOverAddition(): + blach = None + bluchi = None + print(bluch) + + def EliminationOverAddition(): + blucha = None + bluc = None + print(bluch) + + for func, suggestion in [(Substitution, "'blech'?"), + (Elimination, "'blch'?"), + (Addition, "'bluchin'?"), + (EliminationOverAddition, "'blucha'?"), + (SubstitutionOverElimination, "'blach'?"), + (SubstitutionOverAddition, "'blach'?")]: + err = None + try: + func() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertIn(suggestion, err.getvalue()) + + def test_name_error_suggestions_from_globals(self): + def func(): + print(global_for_suggestio) + try: + func() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertIn("'global_for_suggestions'?", err.getvalue()) + + def test_name_error_suggestions_from_builtins(self): + def func(): + print(ZeroDivisionErrrrr) + try: + func() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertIn("'ZeroDivisionError'?", err.getvalue()) + + def test_name_error_suggestions_do_not_trigger_for_long_names(self): + def f(): + somethingverywronghehehehehehe = None + print(somethingverywronghe) + + try: + f() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("somethingverywronghehe", err.getvalue()) + + def test_name_error_bad_suggestions_do_not_trigger_for_small_names(self): + vvv = mom = w = id = pytho = None + + with self.subTest(name="b"): + try: + b + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="v"): + try: + v + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="m"): + try: + m + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="py"): + try: + py + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + def test_name_error_suggestions_do_not_trigger_for_too_many_locals(self): + def f(): + # Mutating locals() is unreliable, so we need to do it by hand + a1 = a2 = a3 = a4 = a5 = a6 = a7 = a8 = a9 = a10 = \ + a11 = a12 = a13 = a14 = a15 = a16 = a17 = a18 = a19 = a20 = \ + a21 = a22 = a23 = a24 = a25 = a26 = a27 = a28 = a29 = a30 = \ + a31 = a32 = a33 = a34 = a35 = a36 = a37 = a38 = a39 = a40 = \ + a41 = a42 = a43 = a44 = a45 = a46 = a47 = a48 = a49 = a50 = \ + a51 = a52 = a53 = a54 = a55 = a56 = a57 = a58 = a59 = a60 = \ + a61 = a62 = a63 = a64 = a65 = a66 = a67 = a68 = a69 = a70 = \ + a71 = a72 = a73 = a74 = a75 = a76 = a77 = a78 = a79 = a80 = \ + a81 = a82 = a83 = a84 = a85 = a86 = a87 = a88 = a89 = a90 = \ + a91 = a92 = a93 = a94 = a95 = a96 = a97 = a98 = a99 = a100 = \ + a101 = a102 = a103 = a104 = a105 = a106 = a107 = a108 = a109 = a110 = \ + a111 = a112 = a113 = a114 = a115 = a116 = a117 = a118 = a119 = a120 = \ + a121 = a122 = a123 = a124 = a125 = a126 = a127 = a128 = a129 = a130 = \ + a131 = a132 = a133 = a134 = a135 = a136 = a137 = a138 = a139 = a140 = \ + a141 = a142 = a143 = a144 = a145 = a146 = a147 = a148 = a149 = a150 = \ + a151 = a152 = a153 = a154 = a155 = a156 = a157 = a158 = a159 = a160 = \ + a161 = a162 = a163 = a164 = a165 = a166 = a167 = a168 = a169 = a170 = \ + a171 = a172 = a173 = a174 = a175 = a176 = a177 = a178 = a179 = a180 = \ + a181 = a182 = a183 = a184 = a185 = a186 = a187 = a188 = a189 = a190 = \ + a191 = a192 = a193 = a194 = a195 = a196 = a197 = a198 = a199 = a200 = \ + a201 = a202 = a203 = a204 = a205 = a206 = a207 = a208 = a209 = a210 = \ + a211 = a212 = a213 = a214 = a215 = a216 = a217 = a218 = a219 = a220 = \ + a221 = a222 = a223 = a224 = a225 = a226 = a227 = a228 = a229 = a230 = \ + a231 = a232 = a233 = a234 = a235 = a236 = a237 = a238 = a239 = a240 = \ + a241 = a242 = a243 = a244 = a245 = a246 = a247 = a248 = a249 = a250 = \ + a251 = a252 = a253 = a254 = a255 = a256 = a257 = a258 = a259 = a260 = \ + a261 = a262 = a263 = a264 = a265 = a266 = a267 = a268 = a269 = a270 = \ + a271 = a272 = a273 = a274 = a275 = a276 = a277 = a278 = a279 = a280 = \ + a281 = a282 = a283 = a284 = a285 = a286 = a287 = a288 = a289 = a290 = \ + a291 = a292 = a293 = a294 = a295 = a296 = a297 = a298 = a299 = a300 = \ + a301 = a302 = a303 = a304 = a305 = a306 = a307 = a308 = a309 = a310 = \ + a311 = a312 = a313 = a314 = a315 = a316 = a317 = a318 = a319 = a320 = \ + a321 = a322 = a323 = a324 = a325 = a326 = a327 = a328 = a329 = a330 = \ + a331 = a332 = a333 = a334 = a335 = a336 = a337 = a338 = a339 = a340 = \ + a341 = a342 = a343 = a344 = a345 = a346 = a347 = a348 = a349 = a350 = \ + a351 = a352 = a353 = a354 = a355 = a356 = a357 = a358 = a359 = a360 = \ + a361 = a362 = a363 = a364 = a365 = a366 = a367 = a368 = a369 = a370 = \ + a371 = a372 = a373 = a374 = a375 = a376 = a377 = a378 = a379 = a380 = \ + a381 = a382 = a383 = a384 = a385 = a386 = a387 = a388 = a389 = a390 = \ + a391 = a392 = a393 = a394 = a395 = a396 = a397 = a398 = a399 = a400 = \ + a401 = a402 = a403 = a404 = a405 = a406 = a407 = a408 = a409 = a410 = \ + a411 = a412 = a413 = a414 = a415 = a416 = a417 = a418 = a419 = a420 = \ + a421 = a422 = a423 = a424 = a425 = a426 = a427 = a428 = a429 = a430 = \ + a431 = a432 = a433 = a434 = a435 = a436 = a437 = a438 = a439 = a440 = \ + a441 = a442 = a443 = a444 = a445 = a446 = a447 = a448 = a449 = a450 = \ + a451 = a452 = a453 = a454 = a455 = a456 = a457 = a458 = a459 = a460 = \ + a461 = a462 = a463 = a464 = a465 = a466 = a467 = a468 = a469 = a470 = \ + a471 = a472 = a473 = a474 = a475 = a476 = a477 = a478 = a479 = a480 = \ + a481 = a482 = a483 = a484 = a485 = a486 = a487 = a488 = a489 = a490 = \ + a491 = a492 = a493 = a494 = a495 = a496 = a497 = a498 = a499 = a500 = \ + a501 = a502 = a503 = a504 = a505 = a506 = a507 = a508 = a509 = a510 = \ + a511 = a512 = a513 = a514 = a515 = a516 = a517 = a518 = a519 = a520 = \ + a521 = a522 = a523 = a524 = a525 = a526 = a527 = a528 = a529 = a530 = \ + a531 = a532 = a533 = a534 = a535 = a536 = a537 = a538 = a539 = a540 = \ + a541 = a542 = a543 = a544 = a545 = a546 = a547 = a548 = a549 = a550 = \ + a551 = a552 = a553 = a554 = a555 = a556 = a557 = a558 = a559 = a560 = \ + a561 = a562 = a563 = a564 = a565 = a566 = a567 = a568 = a569 = a570 = \ + a571 = a572 = a573 = a574 = a575 = a576 = a577 = a578 = a579 = a580 = \ + a581 = a582 = a583 = a584 = a585 = a586 = a587 = a588 = a589 = a590 = \ + a591 = a592 = a593 = a594 = a595 = a596 = a597 = a598 = a599 = a600 = \ + a601 = a602 = a603 = a604 = a605 = a606 = a607 = a608 = a609 = a610 = \ + a611 = a612 = a613 = a614 = a615 = a616 = a617 = a618 = a619 = a620 = \ + a621 = a622 = a623 = a624 = a625 = a626 = a627 = a628 = a629 = a630 = \ + a631 = a632 = a633 = a634 = a635 = a636 = a637 = a638 = a639 = a640 = \ + a641 = a642 = a643 = a644 = a645 = a646 = a647 = a648 = a649 = a650 = \ + a651 = a652 = a653 = a654 = a655 = a656 = a657 = a658 = a659 = a660 = \ + a661 = a662 = a663 = a664 = a665 = a666 = a667 = a668 = a669 = a670 = \ + a671 = a672 = a673 = a674 = a675 = a676 = a677 = a678 = a679 = a680 = \ + a681 = a682 = a683 = a684 = a685 = a686 = a687 = a688 = a689 = a690 = \ + a691 = a692 = a693 = a694 = a695 = a696 = a697 = a698 = a699 = a700 = \ + a701 = a702 = a703 = a704 = a705 = a706 = a707 = a708 = a709 = a710 = \ + a711 = a712 = a713 = a714 = a715 = a716 = a717 = a718 = a719 = a720 = \ + a721 = a722 = a723 = a724 = a725 = a726 = a727 = a728 = a729 = a730 = \ + a731 = a732 = a733 = a734 = a735 = a736 = a737 = a738 = a739 = a740 = \ + a741 = a742 = a743 = a744 = a745 = a746 = a747 = a748 = a749 = a750 = \ + a751 = a752 = a753 = a754 = a755 = a756 = a757 = a758 = a759 = a760 = \ + a761 = a762 = a763 = a764 = a765 = a766 = a767 = a768 = a769 = a770 = \ + a771 = a772 = a773 = a774 = a775 = a776 = a777 = a778 = a779 = a780 = \ + a781 = a782 = a783 = a784 = a785 = a786 = a787 = a788 = a789 = a790 = \ + a791 = a792 = a793 = a794 = a795 = a796 = a797 = a798 = a799 = a800 \ + = None + print(a0) + + try: + f() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("a1", err.getvalue()) + + def test_name_error_with_custom_exceptions(self): + def f(): + blech = None + raise NameError() + + try: + f() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def f(): + blech = None + raise NameError + + try: + f() + except NameError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def test_unbound_local_error_doesn_not_match(self): + def foo(): + something = 3 + print(somethong) + somethong = 3 + + try: + foo() + except UnboundLocalError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("something", err.getvalue()) + + +class AttributeErrorTests(unittest.TestCase): + def test_attributes(self): + # Setting 'attr' should not be a problem. + exc = AttributeError('Ouch!') + self.assertIsNone(exc.name) + self.assertIsNone(exc.obj) + + sentinel = object() + exc = AttributeError('Ouch', name='carry', obj=sentinel) + self.assertEqual(exc.name, 'carry') + self.assertIs(exc.obj, sentinel) + + def test_getattr_has_name_and_obj(self): + class A: + blech = None + + obj = A() + try: + obj.bluch + except AttributeError as exc: + self.assertEqual("bluch", exc.name) + self.assertEqual(obj, exc.obj) + + def test_getattr_has_name_and_obj_for_method(self): + class A: + def blech(self): + return + + obj = A() + try: + obj.bluch() + except AttributeError as exc: + self.assertEqual("bluch", exc.name) + self.assertEqual(obj, exc.obj) + + def test_getattr_suggestions(self): + class Substitution: + noise = more_noise = a = bc = None + blech = None + + class Elimination: + noise = more_noise = a = bc = None + blch = None + + class Addition: + noise = more_noise = a = bc = None + bluchin = None + + class SubstitutionOverElimination: + blach = None + bluc = None + + class SubstitutionOverAddition: + blach = None + bluchi = None + + class EliminationOverAddition: + blucha = None + bluc = None + + for cls, suggestion in [(Substitution, "'blech'?"), + (Elimination, "'blch'?"), + (Addition, "'bluchin'?"), + (EliminationOverAddition, "'bluc'?"), + (SubstitutionOverElimination, "'blach'?"), + (SubstitutionOverAddition, "'blach'?")]: + try: + cls().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn(suggestion, err.getvalue()) + + def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + class A: + blech = None + + try: + A().somethingverywrong + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + class MyClass: + vvv = mom = w = id = pytho = None + + with self.subTest(name="b"): + try: + MyClass.b + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="v"): + try: + MyClass.v + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="m"): + try: + MyClass.m + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + with self.subTest(name="py"): + try: + MyClass.py + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + self.assertNotIn("you mean", err.getvalue()) + self.assertNotIn("vvv", err.getvalue()) + self.assertNotIn("mom", err.getvalue()) + self.assertNotIn("'id'", err.getvalue()) + self.assertNotIn("'w'", err.getvalue()) + self.assertNotIn("'pytho'", err.getvalue()) + + + def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + class A: + blech = None + # A class with a very big __dict__ will not be consider + # for suggestions. + for index in range(2000): + setattr(A, f"index_{index}", None) + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + + def test_getattr_suggestions_no_args(self): + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError() + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn("blech", err.getvalue()) + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError + + try: + A().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn("blech", err.getvalue()) + + def test_getattr_suggestions_invalid_args(self): + class NonStringifyClass: + __str__ = None + __repr__ = None + + class A: + blech = None + def __getattr__(self, attr): + raise AttributeError(NonStringifyClass()) + + class B: + blech = None + def __getattr__(self, attr): + raise AttributeError("Error", 23) + + class C: + blech = None + def __getattr__(self, attr): + raise AttributeError(23) + + for cls in [A, B, C]: + try: + cls().bluch + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertIn("blech", err.getvalue()) + + def test_getattr_suggestions_for_same_name(self): + class A: + def __dir__(self): + return ['blech'] + try: + A().blech + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("Did you mean", err.getvalue()) + + def test_attribute_error_with_failing_dict(self): + class T: + bluch = 1 + def __dir__(self): + raise AttributeError("oh no!") + + try: + T().blich + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("blech", err.getvalue()) + self.assertNotIn("oh no!", err.getvalue()) + + def test_attribute_error_with_bad_name(self): + try: + raise AttributeError(name=12, obj=23) + except AttributeError as exc: + with support.captured_stderr() as err: + sys.__excepthook__(*sys.exc_info()) + + self.assertNotIn("?", err.getvalue()) class ImportErrorTests(unittest.TestCase): From 1bff6a38d1598ff41bdb0b66cf7b5665fe07964f Mon Sep 17 00:00:00 2001 From: snowapril Date: Mon, 11 Oct 2021 11:30:20 +0900 Subject: [PATCH 6/7] add decorators on failed tests Signed-off-by: snowapril --- Lib/test/test_exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 5d9d36589..dab99f154 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1605,6 +1605,8 @@ class NameErrorTests(unittest.TestCase): class AttributeErrorTests(unittest.TestCase): + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_attributes(self): # Setting 'attr' should not be a problem. exc = AttributeError('Ouch!') From 46347aa6d46c6cb6312f42dea2bfdcf46f76cbae Mon Sep 17 00:00:00 2001 From: snowapril Date: Tue, 12 Oct 2021 23:52:10 +0900 Subject: [PATCH 7/7] fix `PyMethod::get` to use `vm.get_attribute` As cpython implementation, if `getattro` slot is not `PyBaseObject::getattro`, then use `vm.get_attribute` not slot method. Signed-off-by: snowapril --- vm/src/pyobject.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/src/pyobject.rs b/vm/src/pyobject.rs index d6dcf0880..d70c1041f 100644 --- a/vm/src/pyobject.rs +++ b/vm/src/pyobject.rs @@ -1047,7 +1047,7 @@ impl PyMethod { let getattro = cls.mro_find_map(|cls| cls.slots.getattro.load()).unwrap(); if getattro as usize != object::PyBaseObject::getattro as usize { drop(cls); - return getattro(obj, name, vm).map(Self::Attribute); + return obj.get_attr(name, vm).map(Self::Attribute); } let mut is_method = false;