From 91349a5fbb53e5fb2517dae0c8c8e7bffbff5b65 Mon Sep 17 00:00:00 2001 From: SErAphLi Date: Fri, 28 Feb 2025 02:46:52 +0800 Subject: [PATCH 1/4] fix: get correct source from redefined class --- dill/source.py | 23 +++++++++++++++++++---- dill/tests/test_source.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/dill/source.py b/dill/source.py index 4b538fa6..4a7d7d7c 100644 --- a/dill/source.py +++ b/dill/source.py @@ -26,7 +26,7 @@ import re from inspect import (getblock, getfile, getmodule, getsourcefile, indentsize, isbuiltin, isclass, iscode, isframe, isfunction, ismethod, - ismodule, istraceback) + ismodule, istraceback, currentframe) from tokenize import TokenError from ._dill import IS_IPYTHON @@ -101,6 +101,19 @@ def _matchlambda(func, line): return True return False +def get_outer_frame(): + """ + Get the outermost frame that is not inside source.py. + Returns the frame and its line number. + """ + frame = currentframe() + if not frame: return None, None + source_file = __file__ + while frame: + if frame.f_code.co_filename != source_file: + return frame, frame.f_lineno + frame = frame.f_back + return None, None def findsource(object): """Return the entire source file and starting line number for an object. @@ -234,25 +247,27 @@ def findsource(object): #XXX: we don't find how the instance was built except AttributeError: pass if isclass(object): + _, lineno = get_outer_frame() + start_lineno = lineno if lineno else len(lines)-1 name = object.__name__ pat = re.compile(r'^(\s*)class\s*' + name + r'\b') # make some effort to find the best matching class definition: # use the one with the least indentation, which is the one # that's most probably not inside a function definition. candidates = [] - for i in range(len(lines)-1,-1,-1): + for i in range(start_lineno,-1,-1): match = pat.match(lines[i]) if match: # if it's at toplevel, it's already the best one if lines[i][0] == 'c': return lines, i # else add whitespace to candidate list - candidates.append((match.group(1), i)) + candidates.append((match.group(1), -i)) if candidates: # this will sort by whitespace, and by line number, # less whitespace first #XXX: should sort high lnum before low candidates.sort() - return lines, candidates[0][1] + return lines, -candidates[0][1] else: raise IOError('could not find class definition') raise IOError('could not find code object') diff --git a/dill/tests/test_source.py b/dill/tests/test_source.py index 12b4519d..9dca5e4d 100644 --- a/dill/tests/test_source.py +++ b/dill/tests/test_source.py @@ -53,6 +53,17 @@ def test_getsource(): assert getsource(Foo) == 'class Foo(object):\n def bar(self, x):\n return x*x+x\n' #XXX: add getsource for _foo, _bar +def test_getsource_redefine(): + class Foobar: + def bar(self,x): + return x*x+x + assert getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x\n' + + class Foobar: + def bar(self,x): + return x*x+x+1 + assert getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x+1\n' + # test itself def test_itself(): assert getimport(getimport)=='from dill.source import getimport\n' @@ -163,6 +174,7 @@ def test_foo(): if __name__ == '__main__': test_getsource() + test_getsource_redefine() test_itself() test_builtin() test_imported() From a780bdb81f2759714a4095367ce871a0b46f05b3 Mon Sep 17 00:00:00 2001 From: SErAphLi Date: Fri, 28 Feb 2025 02:58:11 +0800 Subject: [PATCH 2/4] fix: start line number index --- dill/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dill/source.py b/dill/source.py index 4a7d7d7c..6a6c17d1 100644 --- a/dill/source.py +++ b/dill/source.py @@ -248,7 +248,7 @@ def findsource(object): except AttributeError: pass if isclass(object): _, lineno = get_outer_frame() - start_lineno = lineno if lineno else len(lines)-1 + start_lineno = lineno-1 if lineno else len(lines)-1 name = object.__name__ pat = re.compile(r'^(\s*)class\s*' + name + r'\b') # make some effort to find the best matching class definition: From bad18bb621bcb6d257e4d6b790b99dd6237e0c42 Mon Sep 17 00:00:00 2001 From: SErAphLi Date: Fri, 28 Feb 2025 03:19:14 +0800 Subject: [PATCH 3/4] fix: get outer frame should check sourcefile --- dill/source.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dill/source.py b/dill/source.py index 6a6c17d1..cae96914 100644 --- a/dill/source.py +++ b/dill/source.py @@ -101,16 +101,15 @@ def _matchlambda(func, line): return True return False -def get_outer_frame(): +def get_outer_frame(sourcefile): """ Get the outermost frame that is not inside source.py. Returns the frame and its line number. """ frame = currentframe() if not frame: return None, None - source_file = __file__ while frame: - if frame.f_code.co_filename != source_file: + if frame.f_code.co_filename == sourcefile: return frame, frame.f_lineno frame = frame.f_back return None, None @@ -247,7 +246,7 @@ def findsource(object): #XXX: we don't find how the instance was built except AttributeError: pass if isclass(object): - _, lineno = get_outer_frame() + _, lineno = get_outer_frame(sourcefile) start_lineno = lineno-1 if lineno else len(lines)-1 name = object.__name__ pat = re.compile(r'^(\s*)class\s*' + name + r'\b') From 304fbd3cb50d8a3ba4e761fd048c22cce4edbe61 Mon Sep 17 00:00:00 2001 From: SErAphLi Date: Sun, 2 Mar 2025 20:51:48 +0800 Subject: [PATCH 4/4] fix: fix incorrect start line no caused by outer frame --- dill/source.py | 44 ++++++++++++++++++++++++--------------- dill/tests/test_source.py | 5 +++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/dill/source.py b/dill/source.py index cae96914..40c41e75 100644 --- a/dill/source.py +++ b/dill/source.py @@ -101,18 +101,6 @@ def _matchlambda(func, line): return True return False -def get_outer_frame(sourcefile): - """ - Get the outermost frame that is not inside source.py. - Returns the frame and its line number. - """ - frame = currentframe() - if not frame: return None, None - while frame: - if frame.f_code.co_filename == sourcefile: - return frame, frame.f_lineno - frame = frame.f_back - return None, None def findsource(object): """Return the entire source file and starting line number for an object. @@ -246,27 +234,49 @@ def findsource(object): #XXX: we don't find how the instance was built except AttributeError: pass if isclass(object): - _, lineno = get_outer_frame(sourcefile) - start_lineno = lineno-1 if lineno else len(lines)-1 name = object.__name__ pat = re.compile(r'^(\s*)class\s*' + name + r'\b') + + # find the first frame that inside sourcefile + frame = currentframe() + while frame and frame.f_code.co_filename != sourcefile: + frame = frame.f_back + + # Starting from the found frame, search upward level by level. + while frame and frame.f_code.co_filename == sourcefile: + lineno = frame.f_lineno if hasattr(frame, 'f_lineno') else None + start_lineno = lineno - 1 if lineno is not None else len(lines) - 1 + candidates = [] + for i in range(start_lineno, -1, -1): + match = pat.match(lines[i]) + if match: + # if it's at toplevel, it's already the best one + if lines[i][0] == 'c': + return lines, i + candidates.append((match.group(1), -i)) + if candidates: + candidates.sort() + return lines, -candidates[0][1] + # If no match is found in the current frame, move up to the previous frame. + frame = frame.f_back + # make some effort to find the best matching class definition: # use the one with the least indentation, which is the one # that's most probably not inside a function definition. candidates = [] - for i in range(start_lineno,-1,-1): + for i in range(len(lines)-1,-1,-1): match = pat.match(lines[i]) if match: # if it's at toplevel, it's already the best one if lines[i][0] == 'c': return lines, i # else add whitespace to candidate list - candidates.append((match.group(1), -i)) + candidates.append((match.group(1), i)) if candidates: # this will sort by whitespace, and by line number, # less whitespace first #XXX: should sort high lnum before low candidates.sort() - return lines, -candidates[0][1] + return lines, candidates[0][1] else: raise IOError('could not find class definition') raise IOError('could not find code object') diff --git a/dill/tests/test_source.py b/dill/tests/test_source.py index 9dca5e4d..964414a1 100644 --- a/dill/tests/test_source.py +++ b/dill/tests/test_source.py @@ -35,6 +35,9 @@ class Bar: pass _bar = Bar() +def _wrap_getsource(obj): + return getsource(obj) + # inspect.getsourcelines # dill.source.getblocks def test_getsource(): assert getsource(f) == 'f = lambda x: x**2\n' @@ -58,11 +61,13 @@ class Foobar: def bar(self,x): return x*x+x assert getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x\n' + assert _wrap_getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x\n' class Foobar: def bar(self,x): return x*x+x+1 assert getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x+1\n' + assert _wrap_getsource(Foobar) == ' class Foobar:\n def bar(self,x):\n return x*x+x+1\n' # test itself def test_itself():