Merge pull request #3263 from Snowapril/suggestion

Implement keyword suggestion on `PyAttributeError` and `PyNameError` from cpython 3.10
This commit is contained in:
Jeong YunWon
2021-10-21 02:30:46 +09:00
committed by GitHub
10 changed files with 770 additions and 22 deletions

View File

@@ -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,534 @@ 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):
# TODO: RUSTPYTHON
@unittest.expectedFailure
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):

View File

@@ -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) => {{

View File

@@ -256,7 +256,7 @@ impl PyRef<PyCode> {
}
#[pyproperty]
fn co_varnames(self, vm: &VirtualMachine) -> PyTupleRef {
pub fn co_varnames(self, vm: &VirtualMachine) -> PyTupleRef {
let varnames = self
.code
.varnames

View File

@@ -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),
}
}
@@ -697,7 +703,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);
@@ -713,7 +722,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:

View File

@@ -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"))]

View File

@@ -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;

View File

@@ -34,7 +34,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(

View File

@@ -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;
@@ -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)
}
}

75
vm/src/suggestion.rs Normal file
View File

@@ -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<Item = &'a PyObjectRef>,
name: &PyObjectRef,
) -> Option<PyStrRef> {
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::<PyStr>()?;
for item in dir_iter {
let item_name = item.downcast_ref::<PyStr>()?;
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<PyStrRef> {
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
}
}

View File

@@ -665,9 +665,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 {
@@ -1371,6 +1376,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<F>(