@@ -41,6 +41,14 @@ def strip(s):
4141 return textwrap .dedent (s ).strip ('\n ' )
4242
4343
44+ def get_prof_stats (prof , name = 'prof' , ** kwargs ):
45+ with io .StringIO () as sio :
46+ prof .print_stats (sio , ** kwargs )
47+ output = sio .getvalue ()
48+ print (f'@{ name } :' , textwrap .indent (output , ' ' ), sep = '\n \n ' )
49+ return output
50+
51+
4452class check_timings :
4553 """
4654 Verify that the profiler starts without timing data and ends with
@@ -252,10 +260,7 @@ def foo(cls) -> str:
252260 assert profile .enable_count == 0
253261 assert len (profile .functions ) == 1
254262 assert Object .foo () == Object ().foo () == 'ObjectObject'
255- with io .StringIO () as sio :
256- profile .print_stats (stream = sio , summarize = True )
257- output = strip (sio .getvalue ())
258- print (output )
263+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
259264 # Check that we have profiled `Object.foo()`
260265 assert output .endswith ('foo' )
261266 line , = (line for line in output .splitlines () if line .endswith ('* 2' ))
@@ -286,10 +291,7 @@ def foo(x: int) -> int:
286291 assert profile .enable_count == 0
287292 assert len (profile .functions ) == 1
288293 assert Object .foo (3 ) == Object ().foo (3 ) == 6
289- with io .StringIO () as sio :
290- profile .print_stats (stream = sio , summarize = True )
291- output = strip (sio .getvalue ())
292- print (output )
294+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
293295 # Check that we have profiled `Object.foo()`
294296 assert output .endswith ('foo' )
295297 line , = (line for line in output .splitlines () if line .endswith ('* 2' ))
@@ -327,10 +329,7 @@ def foo(self, x: int) -> int:
327329 == profiled_foo_2 (2 )
328330 == obj .foo (2 )
329331 == id (obj ) * 2 )
330- with io .StringIO () as sio :
331- profile .print_stats (stream = sio , summarize = True )
332- output = strip (sio .getvalue ())
333- print (output )
332+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
334333 # Check that we have profiled `Object.foo()`
335334 assert output .endswith ('foo' )
336335 line , = (line for line in output .splitlines () if line .endswith ('* x' ))
@@ -363,10 +362,7 @@ def foo(self, x: int) -> int:
363362 assert profile .enable_count == 0
364363 assert profile .functions == [Object .foo ]
365364 assert obj .foo (1 ) == obj .bar () == id (obj )
366- with io .StringIO () as sio :
367- profile .print_stats (stream = sio , summarize = True )
368- output = strip (sio .getvalue ())
369- print (output )
365+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
370366 # Check that we have profiled `Object.foo()` (via `.bar()`)
371367 assert output .endswith ('foo' )
372368 line , = (line for line in output .splitlines () if line .endswith ('* x' ))
@@ -403,10 +399,7 @@ def foo(x: int, y: int) -> int:
403399 == bar (3 )
404400 == foo (2 , 3 )
405401 == 5 )
406- with io .StringIO () as sio :
407- profile .print_stats (stream = sio , summarize = True )
408- output = strip (sio .getvalue ())
409- print (output )
402+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
410403 # Check that we have profiled `foo()`
411404 assert output .endswith ('foo' )
412405 line , = (line for line in output .splitlines () if line .endswith ('x + y' ))
@@ -452,10 +445,7 @@ def foo(self, foo) -> None:
452445 obj .foo = 10 # Use setter
453446 assert obj .x == 5
454447 assert obj .foo == 10 # Use getter
455- with io .StringIO () as sio :
456- profile .print_stats (stream = sio , summarize = True )
457- output = strip (sio .getvalue ())
458- print (output )
448+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
459449 # Check that we have profiled `Object.foo`
460450 assert output .endswith ('foo' )
461451 getter_line , = (line for line in output .splitlines ()
@@ -495,9 +485,7 @@ def foo(self) -> int:
495485 obj = Object (3 )
496486 assert obj .foo == 6 # Use getter
497487 assert obj .foo == 6 # Getter not called because it's cached
498- with io .StringIO () as sio :
499- profile .print_stats (stream = sio , summarize = True )
500- output = strip (sio .getvalue ())
488+ output = strip (get_prof_stats (profile , name = 'profile' , summarize = True ))
501489 # Check that we have profiled `Object.foo`
502490 assert output .endswith ('foo' )
503491 line , = (line for line in output .splitlines () if line .endswith ('* 2' ))
@@ -713,10 +701,7 @@ def test_profile_generated_code():
713701 profiled_fn = profiler (fn )
714702 profiled_fn ()
715703
716- import io
717- s = io .StringIO ()
718- profiler .print_stats (stream = s )
719- output = s .getvalue ()
704+ output = get_prof_stats (profiler , 'profiler' )
720705
721706 # Check that the output contains the generated code's source lines
722707 for line in code_lines :
@@ -900,13 +885,13 @@ def check_has_profiling_data(name, output, func_id, expected):
900885
901886 if force_same_line_numbers :
902887 funcs = {}
903- pattern = textwrap . dedent ("""
888+ pattern = strip ("""
904889 def func{0}():
905890 result = []
906891 for _ in range({0}):
907892 result.append({0})
908893 return result
909- """ ). strip ( ' \n ' )
894+ """ )
910895 for i in [1 , 2 , 3 , 4 ]:
911896 tempfile = tmp_path / f'func{ i } .py'
912897 source = pattern .format (i )
@@ -964,11 +949,41 @@ def func4():
964949 for func in profiled_funcs }) == len (profiled_funcs )
965950 # Check the profiling results
966951 for name , prof in sorted (all_profs .items ()):
967- with io .StringIO () as sio :
968- prof .print_stats (sio , summarize = True )
969- output = sio .getvalue ()
970- print (f'@{ name } :' , textwrap .indent (output , ' ' ), sep = '\n \n ' )
952+ output = get_prof_stats (prof , name = name , summarize = True )
971953 for func_id , decs in all_dec_names .items ():
972954 profiled = name in decs
973955 check_seen (name , output , func_id , profiled )
974956 check_has_profiling_data (name , output , func_id , profiled )
957+
958+
959+ def test_aggregate_profiling_data_between_code_versions ():
960+ """
961+ Test that profiling data from previous versions of the code object
962+ are preserved when another profiler causes the code object of a
963+ function to be overwritten.
964+ """
965+ def func (n ):
966+ x = 0
967+ for n in range (1 , n + 1 ):
968+ x += n
969+ return x
970+
971+ prof1 = LineProfiler ()
972+ prof2 = LineProfiler ()
973+
974+ # Gather data with `@prof1`
975+ wrapper1 = prof1 (func )
976+ assert wrapper1 (10 ) == 10 * 11 // 2
977+ code = func .__code__
978+ # Gather data with `@prof2`; the code object is overwritten here
979+ wrapper2 = prof2 (wrapper1 )
980+ assert func .__code__ != code
981+ assert wrapper2 (15 ) == 15 * 16 // 2
982+ # Despite the overwrite of the code object, the old data should
983+ # still remain, and be aggregated with the new data when calling
984+ # `prof1.get_stats()`
985+ for prof , name , count in (prof1 , 'prof1' , 25 ), (prof2 , 'prof2' , 15 ):
986+ result = get_prof_stats (prof , name )
987+ loop_body = next (line for line in result .splitlines ()
988+ if line .endswith ('x += n' ))
989+ assert loop_body .split ()[1 ] == str (count )
0 commit comments