2016-07-06 4 views
3

print() 문장을 가진 파이썬 스크립트가 주어지면 스크립트를 실행하고 각각의 결과를 보여주는 각 문장 다음에 주석을 삽입하고 싶습니다.주석으로 파이썬 print() 출력에 주석 달기

a, b = 1, 2 

print('a + b:', a + b) 

c, d = 3, 4 

print('c + d:', c + d) 

가 원하는 출력은 다음과 같습니다 : 입증하기 위해, example.py 이름이 스크립트를 가지고

import sys 
from io import StringIO 

def intercept_stdout(func): 
    "redirect stdout from a target function" 
    def wrapper(*args, **kwargs): 
     "wrapper function for intercepting stdout" 
     # save original stdout 
     original_stdout = sys.stdout 

     # set up StringIO object to temporarily capture stdout 
     capture_stdout = StringIO() 
     sys.stdout = capture_stdout 

     # execute wrapped function 
     func(*args, **kwargs) 

     # assign captured stdout to value 
     func_output = capture_stdout.getvalue() 

     # reset stdout 
     sys.stdout = original_stdout 

     # return captured value 
     return func_output 

    return wrapper 


@intercept_stdout 
def exec_target(name): 
    "execute a target script" 
    with open(name, 'r') as f:  
     exec(f.read()) 


def read_target(name): 
    "read source code from a target script & return it as a list of lines" 
    with open(name) as f: 
     source = f.readlines() 

    # to properly format last comment, ensure source ends in a newline 
    if len(source[-1]) >= 1 and source[-1][-1] != '\n': 
     source[-1] += '\n' 

    return source 


def annotate_source(target): 
    "given a target script, return the source with comments under each print()" 
    target_source = read_target(target) 

    # find each line that starts with 'print(' & get indices in reverse order 
    print_line_indices = [i for i, j in enumerate(target_source) 
           if len(j) > 6 and j[:6] == 'print('] 
    print_line_indices.reverse() 

    # execute the target script and get each line output in reverse order 
    target_output = exec_target(target) 
    printed_lines = target_output.split('\n') 
    printed_lines.reverse() 

    # iterate over the source and insert commented target output line-by-line 
    annotated_source = [] 
    for i, line in enumerate(target_source): 
     annotated_source.append(line) 
     if print_line_indices and i == print_line_indices[-1]: 
      annotated_source.append('# ' + printed_lines.pop() + '\n') 
      print_line_indices.pop() 

    # return new annotated source as a string 
    return ''.join(annotated_source) 


if __name__ == '__main__': 
    target_script = 'example.py' 
    with open('annotated_example.py', 'w') as f: 
     f.write(annotate_source(target_script)) 

:

a, b = 1, 2 

print('a + b:', a + b) 
# a + b: 3 

c, d = 3, 4 

print('c + d:', c + d) 
# c + d: 7 

다음은 위와 같은 간단한 예제를 작동 나의 시도이다 그러나 print() 문이 여러 줄에 걸쳐있는 스크립트와 print()은 줄 시작 부분에 있지 않습니다. 최상의 경우 시나리오에서는 함수 내에서 print() 문에 대해서도 작동합니다. 다음 예를 보자

print('''print to multiple lines, first line 
second line 
third line''') 

print('print from partial line, first part') if True else 0 

1 if False else print('print from partial line, second part') 

print('print from compound statement, first part'); pass 

pass; print('print from compound statement, second part') 

def foo(): 
    print('bar') 

foo() 

이상적를, 출력은 다음과 같습니다

print('''print to multiple lines, first line 
second line 
third line''') 
# print to multiple lines, first line 
# second line 
# third line 

print('print from partial line, first part') if True else 0 
# print from partial line, first part 

1 if False else print('print from partial line, second part') 
# print from partial line, second part 

print('print from compound statement, first part'); pass 
# print from compound statement, first part 

pass; print('print from compound statement, second part') 
# print from compound statement, second part 

def foo(): 
    print('bar') 

foo() 
# bar 

그러나 스크립트가 위과 같이 그것을 미치게 :

이 과정을 만들 것입니다 무엇 접근
print('''print to multiple lines, first line 
# print to multiple lines, first line 
second line 
third line''') 

print('print from partial line, first part') if True else 0 
# second line 

1 if False else print('print from partial line, second part') 

print('print from compound statement, first part'); pass 
# third line 

pass; print('print from compound statement, second part') 

def foo(): 
    print('bar') 

foo() 

더 튼튼한가? 공급 때

+3

'def foo (a, b) : print (a, b)'와 같은 상황에서'foo'가 여러 번 호출 될 수 있다고 예상 할 수있는 것은 무엇입니까? – Brian

+1

미리 값을 모르는 곳에 인쇄물을 어떻게 표시하려고합니까? ex print (randint (0,100))? – xgord

+0

@xgord 여전히 표시되지만 각 런스 루어마다 다를 수 있습니다. 나는 매번 결과가 같은 경우에 주로 사용하는 것이지만, 예제 출력을 보여주기에는 여전히 유용 할 수 있습니다. – Alec

답변

5

당신이 inspect 모듈을 사용하여 생각 해 봤나 :

는 두 번째 매개 변수 (documentation)를 사용해야 다음 exec()에 전역을 수행하려면? 최상위 호출 옆에 항상 주석을 넣고 주석을 달고있는 파일이 충분히 간단하다고 말하고 싶다면 합리적인 결과를 얻을 수 있습니다.

: 나는 다음과 같은 파일 "foo.py"를 실행하면

import inspect 
import sys 
from io import StringIO 

file_changes = {} 

def anno_print(old_print, *args, **kwargs): 
    (frame, filename, line_number, 
    function_name, lines, index) = inspect.getouterframes(inspect.currentframe())[-2] 
    if filename not in file_changes: 
     file_changes[filename] = {} 
    if line_number not in file_changes[filename]: 
     file_changes[filename][line_number] = [] 
    orig_stdout = sys.stdout 
    capture_stdout = StringIO() 
    sys.stdout = capture_stdout 
    old_print(*args, **kwargs) 
    output = capture_stdout.getvalue() 
    file_changes[filename][line_number].append(output) 
    sys.stdout = orig_stdout 
    return 

def make_annotated_file(old_source, new_source): 
    changes = file_changes[old_source] 
    old_source_F = open(old_source) 
    new_source_F = open(new_source, 'w') 
    content = old_source_F.readlines() 
    for i in range(len(content)): 
     line_num = i + 1 
     new_source_F.write(content[i]) 
     if content[i][-1] != '\n': 
      new_source_F.write('\n') 
     if line_num in changes: 
      for output in changes[line_num]: 
       output = output[:-1].replace('\n', '\n#') + '\n' 
       new_source_F.write("#" + output) 
    new_source_F.close() 



if __name__=='__main__': 
    target_source = "foo.py" 
    old_print = __builtins__.print 
    __builtins__.print = lambda *args, **kwargs: anno_print(old_print, *args, **kwargs) 
    with open(target_source) as f: 
     code = compile(f.read(), target_source, 'exec') 
     exec(code) 
    __builtins__.print = old_print 
    make_annotated_file(target_source, "foo_annotated.py") 

: 다음은 인쇄가 호출 결정하기 위해 스택 추적에서 인쇄 기능 내장 무시하고 보이는 내 시도이다

def foo(): 
    print("a") 
    print("b") 

def cool(): 
    foo() 
    print("c") 

def doesnt_print(): 
    a = 2 + 3 

print(1+2) 
foo() 
doesnt_print() 
cool() 

는 출력 "foo_annotated.py": 그것은 최초의 A 블록을 완성되므로

그것은 except SyntaxError 같다
def foo(): 
    print("a") 
    print("b") 

def cool(): 
    foo() 
    print("c") 

def doesnt_print(): 
    a = 2 + 3 

print(1+2) 
#3 
foo() 
#a 
#b 
doesnt_print() 
cool() 
#a 
#b 
#c 
+0

정말 멋지다! 'inspect.getouterframes()'는 훌륭한 접근법처럼 보입니다. 내가 한 것처럼'stdout'을 쓰는 것보다는'print()'를 직접 오버라이드 (override)하기로 결정했다. 'print()'내부의 문자열이 원래 질문의 두 번째 예제에서와 같이 여러 행에 걸쳐있는 경우에만 발견되었습니다. – Alec

+1

오, 예, 주석이 달린 파일에 쓸 때 서식을 지정하는 문제입니다. 'output = output [: - 1] .replace ('\ n', '\ n #') + '\ n'' 나는 여러 줄에서 작동 할 것이라고 생각한다. 지금 인쇄하십시오. –

+0

고쳐 주셔서 감사합니다! 한가지 더 작은 일 : 지금 (다른 사람들을 실행할 때뿐만 아니라) 예제를 실행할 때 마지막 줄 기능에 대한 첫 번째 주석 (예 :'cool() # a')을 얻습니다. 어떤 생각인지 알 겠어? – Alec

1

감사 @Lennart의 의견에, 나는 거의가 작동 가지고 을했습니다 ... 그것은 라인 별을 반복은 더 길고 더 이상 차단 한으로 응집 라인은 현재 블록은 SyntaxError 포함로 exec(). 여기에 다른 사람에게 유용 할 경우를위한 것입니다 :

import sys 
from io import StringIO 

def intercept_stdout(func): 
    "redirect stdout from a target function" 
    def wrapper(*args, **kwargs): 
     "wrapper function for intercepting stdout" 
     # save original stdout 
     original_stdout = sys.stdout 

     # set up StringIO object to temporarily capture stdout 
     capture_stdout = StringIO() 
     sys.stdout = capture_stdout 

     # execute wrapped function 
     func(*args, **kwargs) 

     # assign captured stdout to value 
     func_output = capture_stdout.getvalue() 

     # reset stdout 
     sys.stdout = original_stdout 

     # return captured value 
     return func_output 

    return wrapper 

@intercept_stdout 
def exec_line(source, block_globals): 
    "execute a target block of source code and get output" 
    exec(source, block_globals) 

def read_target(name): 
    "read source code from a target script & return it as a list of lines" 
    with open(name) as f: 
     source = f.readlines() 

    # to properly format last comment, ensure source ends in a newline 
    if len(source[-1]) >= 1 and source[-1][-1] != '\n': 
     source[-1] += '\n' 

    return source 

def get_blocks(target, block_globals): 
    "get outputs for each block of code in source" 
    outputs = [] 
    lines = 1 

    @intercept_stdout 
    def eval_blocks(start_index, end_index, full_source, block_globals): 
     "work through a group of lines of source code and exec each block" 
     nonlocal lines 
     try:  
      exec(''.join(full_source[start_index:end_index]), block_globals) 
     except SyntaxError: 
      lines += 1 
      eval_blocks(start_index, start_index + lines, 
         full_source, block_globals) 

    for i, s in enumerate(target): 
     if lines > 1: 
      lines -= 1 
      continue 
     outputs.append((eval_blocks(i, i+1, target, block_globals), i, lines)) 

    return [(i[1], i[1] + i[2]) for i in outputs] 

def annotate_source(target, block_globals={}): 
    "given a target script, return the source with comments under each print()" 
    target_source = read_target(target) 

    # get each block's start and end indices 
    outputs = get_blocks(target_source, block_globals) 
    code_blocks = [''.join(target_source[i[0]:i[1]]) for i in outputs] 

    # iterate through each 
    annotated_source = [] 
    for c in code_blocks: 
     annotated_source.append(c) 
     printed_lines = exec_line(c, block_globals).split('\n') 
     if printed_lines and printed_lines[-1] == '': 
      printed_lines.pop() 
     for line in printed_lines: 
      annotated_source.append('# ' + line + '\n') 

    # return new annotated source as a string 
    return ''.join(annotated_source) 

def main(): 
    ### script to format goes here 
    target_script = 'example.py' 

    ### name of formatted script goes here 
    new_script = 'annotated_example.py' 

    new_code = annotate_source(target_script) 
    with open(new_script, 'w') as f: 
     f.write(new_code) 

if __name__ == '__main__': 
    main() 

위의 두 예제 각각에 대해 작동합니다. 그러나 다음 실행하려고 할 때 :

def foo(): 
    print('bar') 
    print('baz') 

foo() 

대신 나에게 원하는 출력을 제공합니다 : 그것은 매우 긴 역 추적에 실패

def foo(): 
    print('bar') 
    print('baz') 

foo() 
# bar 
# baz 

: 이런 같은

Traceback (most recent call last): 
    File "ex.py", line 55, in eval_blocks 
    exec(''.join(full_source[start_index:end_index]), block_globals) 
    File "<string>", line 1 
    print('baz') 
    ^
IndentationError: unexpected indent 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "ex.py", line 55, in eval_blocks 
    exec(''.join(full_source[start_index:end_index]), block_globals) 
    File "<string>", line 1 
    print('baz') 
    ^
IndentationError: unexpected indent 

During handling of the above exception, another exception occurred: 

... 

Traceback (most recent call last): 
    File "ex.py", line 55, in eval_blocks 
    exec(''.join(full_source[start_index:end_index]), block_globals) 
    File "<string>", line 1 
    print('baz') 
    ^
IndentationError: unexpected indent 

During handling of the above exception, another exception occurred: 

Traceback (most recent call last): 
    File "ex.py", line 102, in <module> 
    main() 
    File "ex.py", line 97, in main 
    new_code = annotate_source(target_script) 
    File "ex.py", line 74, in annotate_source 
    outputs = get_blocks(target_source, block_globals) 
    File "ex.py", line 65, in get_blocks 
    outputs.append((eval_blocks(i, i+1, target, block_globals), i, lines)) 
    File "ex.py", line 16, in wrapper 
    func(*args, **kwargs) 
    File "ex.py", line 59, in eval_blocks 
    full_source, block_globals) 
    File "ex.py", line 16, in wrapper 
    func(*args, **kwargs) 

... 

    File "ex.py", line 16, in wrapper 
    func(*args, **kwargs) 
    File "ex.py", line 55, in eval_blocks 
    exec(''.join(full_source[start_index:end_index]), block_globals) 
RecursionError: maximum recursion depth exceeded while calling a Python object 

이 보이는 def foo(): print('bar')이 유효한 코드이므로 print('baz')이 함수에 포함되지 않아 IndentationError으로 실패합니다. 이 문제를 피하는 방법에 대한 아이디어가 있습니까? 위의 제안대로 ast으로 다이빙을해야하지만 더 많은 입력이나 사용 예를 좋아할 것으로 판단됩니다.

1

기존 파이썬 파서를 사용하여 코드에서 최상위 수준의 문을 추출하면 훨씬 쉽게 만들 수 있습니다. 예를 들어 표준 라이브러리의 ast 모듈. 그러나 ast는 의견과 같은 정보를 잃어 버립니다.

여기에서 소스 코드 변환으로 작성된 라이브러리가 더 적합 할 수 있습니다. redbaron은 좋은 예입니다.

environment = {} 
for statement in statements: 
    exec(statement, environment) 
+0

ast와 redbaron에 대한 훌륭한 제안 (이전에'ast.literal_eval() '만 사용 했었고, 좀 더 진보 된 기능을 소화해야만했습니다). 환경을'exec()'에서 추출하여 함께 연결할 수 있습니까? – Alec

+1

확실히, exec는 전달한 사전을 수정합니다. 그래서 당신이 exec에 빈 사전을 줄 때, 나중에 환경을 포함 할 것입니다. – Lennart

+0

그건 안심입니다! (나는'exec()'의 디폴트'None' 리턴 값을 가로 챌 필요가 있다고 걱정했다.) – Alec

1

는 전체 기능을위한 충분한 체크하지 않은 구문 오류가 생성되지 않습니다. 당신이 원하는 것은 전체 기능이 동일한 블록에 포함되어 있는지 확인하는 것입니다. 이렇게하려면 다음을 수행하십시오.

  • 현재 블록이 기능인지 확인하십시오. 첫 번째 줄이 def으로 시작하는지 확인하십시오.

  • full_source의 다음 줄이 함수의 두 번째 줄 (들여 쓰기를 정의하는 줄)과 같거나 더 큰 공백으로 시작하는지 확인하십시오. 이는 eval_blocks이 코드의 다음 줄이 더 높거나 같은 간격을 가지는지 여부를 확인하여 함수 안에 있는지 확인합니다.

get_blocks의 코드를 다음과 같이 보일 수 있습니다 : 함수의 마지막 줄은 비록 파일의 끝이었다 경우

# function for finding num of spaces at beginning (could be in global spectrum) 
def get_front_whitespace(string): 
    spaces = 0 
    for char in string: 
     # end loop at end of spaces 
     if char not in ('\t', ' '): 
      break 
     # a tab is equal to 8 spaces 
     elif char == '\t': 
      spaces += 8 
     # otherwise must be a space 
     else: 
      spaces += 1 
    return spaces 

... 

def get_blocks(target, block_globals): 
    "get outputs for each block of code in source" 
    outputs = [] 
    lines = 1 
    # variable to check if current block is a function 
    block_is_func = False 

    @intercept_stdout 
    def eval_blocks(start_index, end_index, full_source, block_globals): 
     "work through a group of lines of source code and exec each block" 
     nonlocal lines 
     nonlocal block_is_func 
     # check if block is a function 
     block_is_func = (full_source[start_index][:3] == 'def') 
     try:  
      exec(''.join(full_source[start_index:end_index]), block_globals) 
     except SyntaxError: 
      lines += 1 
      eval_blocks(start_index, start_index + lines, 
         full_source, block_globals) 
     else: 
      # if the block is a function, check for indents 
      if block_is_func: 
       # get number of spaces in first indent of function 
       func_indent= get_front_whitespace(full_source[start_index + 1]) 
       # get number of spaces in the next index 
       next_index_spaces = get_front_whitespace(full_source[end_index + 1]) 
       # if the next line is equally or more indented than the function indent, continue to next recursion layer 
       if func_indent >= next_index_spaces: 
        lines += 1 
        eval_blocks(start_index, start_index + lines, 
           full_source, block_globals) 

    for i, s in enumerate(target): 
     # reset the function variable for next block 
     if block_is_func: block_is_func = False 
     if lines > 1: 
      lines -= 1 
      continue 
     outputs.append((eval_blocks(i, i+1, target, block_globals), i, lines)) 

    return [(i[1], i[1] + i[2]) for i in outputs] 

이 때문에 전방 인덱싱, 인덱스 오류를 만들 수 있습니다 end_index_spaces = get_front_whitespace(full_source[end_index + 1])

에서 이것은 또한 같은 문제가있을 수있는 선택 제표 및 루프에 사용될 수 있습니다 : 단지 0123의 시작 부분에 ifforwhile 확인라인뿐만 아니라 def도 있습니다. 이것은 들여 쓰기 된 영역 뒤에 코멘트가 생기지 만, 들여 쓰기 된 영역 내부의 인쇄 된 출력은 그것을 호출하는 데 사용되는 변수에 따라 다르므로, 들여 쓰기를 벗어난 출력은 어떤 경우에도 필요하다고 생각합니다.