From 8d507a6fbc19bb51e86799cd74cbbaca87627847 Mon Sep 17 00:00:00 2001 From: snowapril Date: Mon, 9 Aug 2021 18:02:13 +0900 Subject: [PATCH 1/6] seq: add __index__ op overriding class support This commit fix three test cases in test_index.py * SeqTestCase.test_index * SeqTestCase.test_wrappers * ListTestCase.test_setdelitem All these three cases' failures were caused by not supporting classes that override __index__ operator when indexing sequence type. This commit implement almost same routine with PR(#2807) which also did not support classes which override __index__ operator in range indexing. Signed-off-by: snowapril --- Lib/test/test_index.py | 6 ------ vm/src/sliceable.rs | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_index.py b/Lib/test/test_index.py index 087cf8f7ad..1fac132595 100644 --- a/Lib/test/test_index.py +++ b/Lib/test/test_index.py @@ -101,8 +101,6 @@ def setUp(self): self.o2 = newstyle() self.n2 = newstyle() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_index(self): self.o.ind = -2 self.n.ind = 2 @@ -140,8 +138,6 @@ def test_repeat(self): self.assertEqual(self.o * self.seq, self.seq * 3) self.assertEqual(self.n * self.seq, self.seq * 2) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_wrappers(self): self.o.ind = 4 self.n.ind = 5 @@ -169,8 +165,6 @@ def test_error(self): class ListTestCase(SeqTestCase, unittest.TestCase): seq = [0,10,20,30,40,50] - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_setdelitem(self): self.o.ind = -2 self.n.ind = 2 diff --git a/vm/src/sliceable.rs b/vm/src/sliceable.rs index c8feb83e40..93511bef59 100644 --- a/vm/src/sliceable.rs +++ b/vm/src/sliceable.rs @@ -349,11 +349,21 @@ impl SequenceIndex { .ok_or_else(|| vm .new_index_error("cannot fit 'int' into an index-sized integer".to_owned())), s @ PySlice => Ok(SequenceIndex::Slice(s)), - obj => Err(vm.new_type_error(format!( - "{} indices must be integers or slices, not {}", - owner_type, - obj.class().name, - ))), + obj => { + let val = vm.to_index(&obj).map_err(|_| vm.new_type_error(format!( + "{} indices must be integers or slices or classes that override __index__ operator, not '{}'", + owner_type, + obj.class().name + )))?; + val.as_bigint() + .to_isize() + .map(SequenceIndex::Int) + .ok_or_else(|| { + vm.new_index_error( + "cannot fit 'int' into an index-sized integer".to_owned(), + ) + }) + } }) } } From 83914a9f5466f44d8eb4ce086d23da8d91a3ee07 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 9 Aug 2021 15:22:31 +0300 Subject: [PATCH 2/6] Fixes infinite recursion on `raise e from e`, refs #2779 --- extra_tests/snippets/builtin_exceptions.py | 154 +++++++++++++++++++++ vm/src/exceptions.rs | 64 ++++++--- 2 files changed, 201 insertions(+), 17 deletions(-) create mode 100644 extra_tests/snippets/builtin_exceptions.py diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py new file mode 100644 index 0000000000..ddc1183d53 --- /dev/null +++ b/extra_tests/snippets/builtin_exceptions.py @@ -0,0 +1,154 @@ +import platform +import sys + + +# Regression to: +# https://github.com/RustPython/RustPython/issues/2779 + +class MyError(Exception): + pass + + +e = MyError('message') + +try: + raise e from e +except MyError as exc: + # It was a segmentation fault before, will print info to stdout: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert isinstance(exc, MyError) + assert exc.__cause__ is e + assert exc.__context__ is None +else: + assert False, 'exception not raised' + +try: + raise ValueError('test') from e +except ValueError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) # ok, will print two excs + assert isinstance(exc, ValueError) + assert exc.__cause__ is e + assert exc.__context__ is None +else: + assert False, 'exception not raised' + + +# New case: +# potential recursion on `__context__` field + +e = MyError('message') + +try: + try: + raise e + except MyError as exc: + raise e + else: + assert False, 'exception not raised' +except MyError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert exc.__cause__ is None + assert exc.__context__ is None +else: + assert False, 'exception not raised' + +e = MyError('message') + +try: + try: + raise e + except MyError as exc: + raise exc + else: + assert False, 'exception not raised' +except MyError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert exc.__cause__ is None + assert exc.__context__ is None +else: + assert False, 'exception not raised' + +e = MyError('message') + +try: + try: + raise e + except MyError as exc: + raise e from e + else: + assert False, 'exception not raised' +except MyError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert exc.__cause__ is e + assert exc.__context__ is None +else: + assert False, 'exception not raised' + +e = MyError('message') + +try: + try: + raise e + except MyError as exc: + raise exc from e + else: + assert False, 'exception not raised' +except MyError as exc: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert exc.__cause__ is e + assert exc.__context__ is None +else: + assert False, 'exception not raised' + + +# New case: +# two exception in a recursion loop + +class SubError(MyError): + pass + +e = MyError('message') +d = SubError('sub') + + +try: + raise e from d +except MyError as exc: + # It was a segmentation fault before, will print info to stdout: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert isinstance(exc, MyError) + assert exc.__cause__ is d + assert exc.__context__ is None +else: + assert False, 'exception not raised' + +e = MyError('message') + +try: + raise d from e +except SubError as exc: + # It was a segmentation fault before, will print info to stdout: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert isinstance(exc, SubError) + assert exc.__cause__ is e + assert exc.__context__ is None +else: + assert False, 'exception not raised' + + +# New case: +# explicit `__context__` manipulation. + +e = MyError('message') +e.__context__ = e + +try: + raise e +except MyError as exc: + # It was a segmentation fault before, will print info to stdout: + if platform.python_implementation() == 'RustPython': + # For some reason `CPython` hangs on this code: + sys.excepthook(type(exc), exc, exc.__traceback__) + assert isinstance(exc, MyError) + assert exc.__cause__ is None + assert exc.__context__ is e diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index dda480756e..5245684951 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -11,12 +11,13 @@ use crate::types::create_type_with_slots; use crate::StaticType; use crate::VirtualMachine; use crate::{ - IntoPyObject, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult, PyValue, - TryFromObject, TypeProtocol, + IdProtocol, IntoPyObject, PyClassImpl, PyContext, PyIterable, PyObjectRef, PyRef, PyResult, + PyValue, TryFromObject, TypeProtocol, }; use crossbeam_utils::atomic::AtomicCell; use itertools::Itertools; +use std::collections::HashSet; use std::fmt; use std::fs::File; use std::io::{self, BufRead, BufReader}; @@ -194,21 +195,8 @@ pub fn write_exception( vm: &VirtualMachine, exc: &PyBaseExceptionRef, ) -> Result<(), W::Error> { - if let Some(cause) = exc.cause() { - write_exception(output, vm, &cause)?; - writeln!( - output, - "\nThe above exception was the direct cause of the following exception:\n" - )?; - } else if let Some(context) = exc.context() { - write_exception(output, vm, &context)?; - writeln!( - output, - "\nDuring handling of the above exception, another exception occurred:\n" - )?; - } - - write_exception_inner(output, vm, exc) + let seen = &mut HashSet::::new(); + write_exception_recursive(output, vm, exc, seen) } fn print_source_line( @@ -253,6 +241,48 @@ fn write_traceback_entry( Ok(()) } +fn write_exception_recursive( + output: &mut W, + vm: &VirtualMachine, + exc: &PyBaseExceptionRef, + seen: &mut HashSet, +) -> Result<(), W::Error> { + // This function should not be called directly, + // use `wite_exception` as a public interface. + // It is similar to `print_exception_recursive` from `CPython`. + seen.insert(exc.as_object().get_id()); + if let Some(cause) = exc.cause() { + // This can be a special case: `raise e from e`, + // we just ignore it and treat like `raise e` without any extra steps. + if !seen.contains(&cause.as_object().get_id()) { + write_exception_recursive(output, vm, &cause, seen)?; + writeln!( + output, + "\nThe above exception was the direct cause of the following exception:\n" + )?; + } else { + seen.insert(cause.as_object().get_id()); + } + } else if let Some(context) = exc.context() { + // This can be a special case: + // e = ValueError('e') + // e.__context__ = e + // In this case, we just ignore + // `__context__` part from going into recursion. + if !seen.contains(&context.as_object().get_id()) { + write_exception_recursive(output, vm, &context, seen)?; + writeln!( + output, + "\nDuring handling of the above exception, another exception occurred:\n" + )?; + } else { + seen.insert(context.as_object().get_id()); + } + } + + write_exception_inner(output, vm, exc) +} + /// Print exception with traceback pub fn write_exception_inner( output: &mut W, From 794d2ce2e474d000e07d0509a839f45757a0c9da Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 10 Aug 2021 03:33:48 +0900 Subject: [PATCH 3/6] Refactor SequenceIndex::try_from_object_for --- vm/src/sliceable.rs | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/vm/src/sliceable.rs b/vm/src/sliceable.rs index 93511bef59..b7b7840965 100644 --- a/vm/src/sliceable.rs +++ b/vm/src/sliceable.rs @@ -341,30 +341,21 @@ impl SequenceIndex { obj: PyObjectRef, owner_type: &'static str, ) -> PyResult { - match_class!(match obj { - i @ PyInt => i - .as_bigint() - .to_isize() - .map(SequenceIndex::Int) - .ok_or_else(|| vm - .new_index_error("cannot fit 'int' into an index-sized integer".to_owned())), - s @ PySlice => Ok(SequenceIndex::Slice(s)), + let idx = match_class!(match obj { + i @ PyInt => i.as_bigint().to_isize(), + s @ PySlice => return Ok(SequenceIndex::Slice(s)), obj => { let val = vm.to_index(&obj).map_err(|_| vm.new_type_error(format!( "{} indices must be integers or slices or classes that override __index__ operator, not '{}'", owner_type, obj.class().name )))?; - val.as_bigint() - .to_isize() - .map(SequenceIndex::Int) - .ok_or_else(|| { - vm.new_index_error( - "cannot fit 'int' into an index-sized integer".to_owned(), - ) - }) + val.as_bigint().to_isize() } - }) + }).ok_or_else(|| { + vm.new_index_error("cannot fit 'int' into an index-sized integer".to_owned()) + })?; + Ok(SequenceIndex::Int(idx)) } } From 91022f580899506e17bd7e8b74a5cc4e8a37984a Mon Sep 17 00:00:00 2001 From: jfh Date: Mon, 9 Aug 2021 22:02:00 +0300 Subject: [PATCH 4/6] Use rustpython_common via exported name. --- derive/src/pyclass.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/derive/src/pyclass.rs b/derive/src/pyclass.rs index cd5c759fbc..530e84093f 100644 --- a/derive/src/pyclass.rs +++ b/derive/src/pyclass.rs @@ -204,8 +204,8 @@ fn generate_class_def( } impl ::rustpython_vm::StaticType for #ident { - fn static_cell() -> &'static ::rustpython_common::static_cell::StaticCell<::rustpython_vm::builtins::PyTypeRef> { - ::rustpython_common::static_cell! { + fn static_cell() -> &'static ::rustpython_vm::common::static_cell::StaticCell<::rustpython_vm::builtins::PyTypeRef> { + ::rustpython_vm::common::static_cell! { static CELL: ::rustpython_vm::builtins::PyTypeRef; } &CELL From a611ba2e45b7377aaa18efe3d04850e342b365d5 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Tue, 10 Aug 2021 04:04:22 +0900 Subject: [PATCH 5/6] handle cause and context with same code block --- vm/src/exceptions.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 5245684951..3102e78002 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -251,32 +251,33 @@ fn write_exception_recursive( // use `wite_exception` as a public interface. // It is similar to `print_exception_recursive` from `CPython`. seen.insert(exc.as_object().get_id()); - if let Some(cause) = exc.cause() { + + #[allow(clippy::manual_map)] + if let Some((cause_or_context, msg)) = if let Some(cause) = exc.cause() { // This can be a special case: `raise e from e`, // we just ignore it and treat like `raise e` without any extra steps. - if !seen.contains(&cause.as_object().get_id()) { - write_exception_recursive(output, vm, &cause, seen)?; - writeln!( - output, - "\nThe above exception was the direct cause of the following exception:\n" - )?; - } else { - seen.insert(cause.as_object().get_id()); - } + Some(( + cause, + "\nThe above exception was the direct cause of the following exception:\n", + )) } else if let Some(context) = exc.context() { // This can be a special case: // e = ValueError('e') // e.__context__ = e // In this case, we just ignore // `__context__` part from going into recursion. - if !seen.contains(&context.as_object().get_id()) { - write_exception_recursive(output, vm, &context, seen)?; - writeln!( - output, - "\nDuring handling of the above exception, another exception occurred:\n" - )?; + Some(( + context, + "\nDuring handling of the above exception, another exception occurred:\n", + )) + } else { + None + } { + if !seen.contains(&cause_or_context.as_object().get_id()) { + write_exception_recursive(output, vm, &cause_or_context, seen)?; + writeln!(output, "{}", msg)?; } else { - seen.insert(context.as_object().get_id()); + seen.insert(cause_or_context.as_object().get_id()); } } From b33f72eb4d329b7ad409ba454b0b800802294cbb Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 9 Aug 2021 23:32:02 +0300 Subject: [PATCH 6/6] Adds `__qualname__` to `PyBoundMethod` --- extra_tests/snippets/builtin_type.py | 187 +++++++++++++++++++++++++++ vm/src/builtins/function.rs | 22 ++++ 2 files changed, 209 insertions(+) diff --git a/extra_tests/snippets/builtin_type.py b/extra_tests/snippets/builtin_type.py index a82c87e6c3..d4c51c4427 100644 --- a/extra_tests/snippets/builtin_type.py +++ b/extra_tests/snippets/builtin_type.py @@ -88,6 +88,13 @@ def c(cls): def s(): pass +assert MyTypeWithMethod.method.__name__ == 'method' +assert MyTypeWithMethod().method.__name__ == 'method' +assert MyTypeWithMethod.clsmethod.__name__ == 'clsmethod' +assert MyTypeWithMethod().clsmethod.__name__ == 'clsmethod' +assert MyTypeWithMethod.stmethod.__name__ == 'stmethod' +assert MyTypeWithMethod().stmethod.__name__ == 'stmethod' + assert MyTypeWithMethod.method.__qualname__ == 'MyTypeWithMethod.method' assert MyTypeWithMethod().method.__qualname__ == 'MyTypeWithMethod.method' assert MyTypeWithMethod.clsmethod.__qualname__ == 'MyTypeWithMethod.clsmethod' @@ -95,6 +102,13 @@ def s(): assert MyTypeWithMethod.stmethod.__qualname__ == 'MyTypeWithMethod.stmethod' assert MyTypeWithMethod().stmethod.__qualname__ == 'MyTypeWithMethod.stmethod' +assert MyTypeWithMethod.N.m.__name__ == 'm' +assert MyTypeWithMethod().N.m.__name__ == 'm' +assert MyTypeWithMethod.N.c.__name__ == 'c' +assert MyTypeWithMethod().N.c.__name__ == 'c' +assert MyTypeWithMethod.N.s.__name__ == 's' +assert MyTypeWithMethod().N.s.__name__ == 's' + assert MyTypeWithMethod.N.m.__qualname__ == 'MyTypeWithMethod.N.m' assert MyTypeWithMethod().N.m.__qualname__ == 'MyTypeWithMethod.N.m' assert MyTypeWithMethod.N.c.__qualname__ == 'MyTypeWithMethod.N.c' @@ -102,6 +116,20 @@ def s(): assert MyTypeWithMethod.N.s.__qualname__ == 'MyTypeWithMethod.N.s' assert MyTypeWithMethod().N.s.__qualname__ == 'MyTypeWithMethod.N.s' +assert MyTypeWithMethod.N().m.__name__ == 'm' +assert MyTypeWithMethod().N().m.__name__ == 'm' +assert MyTypeWithMethod.N().c.__name__ == 'c' +assert MyTypeWithMethod().N().c.__name__ == 'c' +assert MyTypeWithMethod.N().s.__name__ == 's' +assert MyTypeWithMethod().N.s.__name__ == 's' + +assert MyTypeWithMethod.N().m.__qualname__ == 'MyTypeWithMethod.N.m' +assert MyTypeWithMethod().N().m.__qualname__ == 'MyTypeWithMethod.N.m' +assert MyTypeWithMethod.N().c.__qualname__ == 'MyTypeWithMethod.N.c' +assert MyTypeWithMethod().N().c.__qualname__ == 'MyTypeWithMethod.N.c' +assert MyTypeWithMethod.N().s.__qualname__ == 'MyTypeWithMethod.N.s' +assert MyTypeWithMethod().N().s.__qualname__ == 'MyTypeWithMethod.N.s' + # Regresesion to # https://github.com/RustPython/RustPython/issues/2775 @@ -126,6 +154,165 @@ def custom_func(): # Regression to +# https://github.com/RustPython/RustPython/issues/2786 + +assert object.__new__.__name__ == '__new__' +assert object.__new__.__qualname__ == 'object.__new__' +assert object.__subclasshook__.__name__ == '__subclasshook__' +assert object.__subclasshook__.__qualname__ == 'object.__subclasshook__' +assert type.__new__.__name__ == '__new__' +assert type.__new__.__qualname__ == 'type.__new__' + + +class AQ: + # To be overridden: + + def one(self): + pass + + @classmethod + def one_cls(cls): + pass + + @staticmethod + def one_st(): + pass + + # To be inherited: + + def two(self): + pass + + @classmethod + def two_cls(cls): + pass + + @staticmethod + def two_st(): + pass + + +class BQ(AQ): + def one(self): + pass + + @classmethod + def one_cls(cls): + pass + + @staticmethod + def one_st(): + pass + + # Extras, defined in subclass: + + def three(self): + pass + + @classmethod + def three_cls(cls): + pass + + @staticmethod + def three_st(): + pass + +assert AQ.one.__name__ == 'one' +assert AQ().one.__name__ == 'one' +assert AQ.one_cls.__name__ == 'one_cls' +assert AQ().one_cls.__name__ == 'one_cls' +assert AQ.one_st.__name__ == 'one_st' +assert AQ().one_st.__name__ == 'one_st' + +assert AQ.one.__qualname__ == 'AQ.one' +assert AQ().one.__qualname__ == 'AQ.one' +assert AQ.one_cls.__qualname__ == 'AQ.one_cls' +assert AQ().one_cls.__qualname__ == 'AQ.one_cls' +assert AQ.one_st.__qualname__ == 'AQ.one_st' +assert AQ().one_st.__qualname__ == 'AQ.one_st' + +assert AQ.two.__name__ == 'two' +assert AQ().two.__name__ == 'two' +assert AQ.two_cls.__name__ == 'two_cls' +assert AQ().two_cls.__name__ == 'two_cls' +assert AQ.two_st.__name__ == 'two_st' +assert AQ().two_st.__name__ == 'two_st' + +assert AQ.two.__qualname__ == 'AQ.two' +assert AQ().two.__qualname__ == 'AQ.two' +assert AQ.two_cls.__qualname__ == 'AQ.two_cls' +assert AQ().two_cls.__qualname__ == 'AQ.two_cls' +assert AQ.two_st.__qualname__ == 'AQ.two_st' +assert AQ().two_st.__qualname__ == 'AQ.two_st' + +assert BQ.one.__name__ == 'one' +assert BQ().one.__name__ == 'one' +assert BQ.one_cls.__name__ == 'one_cls' +assert BQ().one_cls.__name__ == 'one_cls' +assert BQ.one_st.__name__ == 'one_st' +assert BQ().one_st.__name__ == 'one_st' + +assert BQ.one.__qualname__ == 'BQ.one' +assert BQ().one.__qualname__ == 'BQ.one' +assert BQ.one_cls.__qualname__ == 'BQ.one_cls' +assert BQ().one_cls.__qualname__ == 'BQ.one_cls' +assert BQ.one_st.__qualname__ == 'BQ.one_st' +assert BQ().one_st.__qualname__ == 'BQ.one_st' + +assert BQ.two.__name__ == 'two' +assert BQ().two.__name__ == 'two' +assert BQ.two_cls.__name__ == 'two_cls' +assert BQ().two_cls.__name__ == 'two_cls' +assert BQ.two_st.__name__ == 'two_st' +assert BQ().two_st.__name__ == 'two_st' + +assert BQ.two.__qualname__ == 'AQ.two' +assert BQ().two.__qualname__ == 'AQ.two' +assert BQ.two_cls.__qualname__ == 'AQ.two_cls' +assert BQ().two_cls.__qualname__ == 'AQ.two_cls' +assert BQ.two_st.__qualname__ == 'AQ.two_st' +assert BQ().two_st.__qualname__ == 'AQ.two_st' + +assert BQ.three.__name__ == 'three' +assert BQ().three.__name__ == 'three' +assert BQ.three_cls.__name__ == 'three_cls' +assert BQ().three_cls.__name__ == 'three_cls' +assert BQ.three_st.__name__ == 'three_st' +assert BQ().three_st.__name__ == 'three_st' + +assert BQ.three.__qualname__ == 'BQ.three' +assert BQ().three.__qualname__ == 'BQ.three' +assert BQ.three_cls.__qualname__ == 'BQ.three_cls' +assert BQ().three_cls.__qualname__ == 'BQ.three_cls' +assert BQ.three_st.__qualname__ == 'BQ.three_st' +assert BQ().three_st.__qualname__ == 'BQ.three_st' + + +class ClassWithNew: + def __new__(cls, *args, **kwargs): + return super().__new__(cls, *args, **kwargs) + + class N: + def __new__(cls, *args, **kwargs): + return super().__new__(cls, *args, **kwargs) + + +assert ClassWithNew.__new__.__qualname__ == 'ClassWithNew.__new__' +assert ClassWithNew().__new__.__qualname__ == 'ClassWithNew.__new__' +assert ClassWithNew.__new__.__name__ == '__new__' +assert ClassWithNew().__new__.__name__ == '__new__' + +assert ClassWithNew.N.__new__.__qualname__ == 'ClassWithNew.N.__new__' +assert ClassWithNew().N.__new__.__qualname__ == 'ClassWithNew.N.__new__' +assert ClassWithNew.N.__new__.__name__ == '__new__' +assert ClassWithNew().N.__new__.__name__ == '__new__' +assert ClassWithNew.N().__new__.__qualname__ == 'ClassWithNew.N.__new__' +assert ClassWithNew().N().__new__.__qualname__ == 'ClassWithNew.N.__new__' +assert ClassWithNew.N().__new__.__name__ == '__new__' +assert ClassWithNew().N().__new__.__name__ == '__new__' + + +# Regression to: # https://github.com/RustPython/RustPython/issues/2762 assert type.__prepare__() == {} diff --git a/vm/src/builtins/function.rs b/vm/src/builtins/function.rs index ea98ce17d3..2c3e6f1914 100644 --- a/vm/src/builtins/function.rs +++ b/vm/src/builtins/function.rs @@ -482,6 +482,28 @@ impl PyBoundMethod { fn module(&self, vm: &VirtualMachine) -> Option { vm.get_attribute(self.function.clone(), "__module__").ok() } + + #[pyproperty(magic)] + fn qualname(&self, vm: &VirtualMachine) -> PyResult { + if self + .function + .isinstance(&vm.ctx.types.builtin_function_or_method_type) + { + // Special case: we work with `__new__`, which is not really a method. + // It is a function, so its `__qualname__` is just `__new__`. + // We need to add object's part manually. + // Note: at the moment, `__new__` in the form of `tp_new_wrapper` + // is the only instance of `builtin_function_or_method_type`. + let obj_name = vm.get_attribute_opt(self.object.clone(), "__qualname__")?; + let obj_name: Option = obj_name.and_then(|o| o.downcast().ok()); + return Ok(vm.ctx.new_str(format!( + "{}.__new__", + obj_name.as_ref().map_or("?", |s| s.as_str()) + ))); + } + + vm.get_attribute(self.function.clone(), "__qualname__") + } } impl PyValue for PyBoundMethod {