Script para eliminar comentarios / documentación de Python

¿Existe un script o herramienta de Python disponible que pueda eliminar comentarios y cadenas de documentación de la fuente de Python?

Debería hacerse cargo de casos como:

""" aas """ def f(): m = { u'x': u'y' } # faake docstring ;) if 1: 'string' >> m if 2: 'string' , m if 3: 'string' > m 

Así que por fin he encontrado un script simple, que usa el módulo tokenize y elimina tokens de comentarios. Parece funcionar bastante bien, excepto que no puedo eliminar las cadenas de documentación en todos los casos. Mira si puedes mejorarlo para eliminar docstrings.

 import cStringIO import tokenize def remove_comments(src): """ This reads tokens using tokenize.generate_tokens and recombines them using tokenize.untokenize, and skipping comment/docstring tokens in between """ f = cStringIO.StringIO(src) class SkipException(Exception): pass processed_tokens = [] last_token = None # go thru all the tokens and try to skip comments and docstrings for tok in tokenize.generate_tokens(f.readline): t_type, t_string, t_srow_scol, t_erow_ecol, t_line = tok try: if t_type == tokenize.COMMENT: raise SkipException() elif t_type == tokenize.STRING: if last_token is None or last_token[0] in [tokenize.INDENT]: # FIXEME: this may remove valid strings too? #raise SkipException() pass except SkipException: pass else: processed_tokens.append(tok) last_token = tok return tokenize.untokenize(processed_tokens) 

También me gustaría probarlo en una gran colección de scripts con una buena cobertura de prueba unitaria. ¿Puede sugerir un proyecto de código abierto de este tipo?

    Esto hace el trabajo:

     """ Strip comments and docstrings from a file. """ import sys, token, tokenize def do_file(fname): """ Run on just one file. """ source = open(fname) mod = open(fname + ",strip", "w") prev_toktype = token.INDENT first_line = None last_lineno = -1 last_col = 0 tokgen = tokenize.generate_tokens(source.readline) for toktype, ttext, (slineno, scol), (elineno, ecol), ltext in tokgen: if 0: # Change to if 1 to see the tokens fly by. print("%10s %-14s %-20r %r" % ( tokenize.tok_name.get(toktype, toktype), "%d.%d-%d.%d" % (slineno, scol, elineno, ecol), ttext, ltext )) if slineno > last_lineno: last_col = 0 if scol > last_col: mod.write(" " * (scol - last_col)) if toktype == token.STRING and prev_toktype == token.INDENT: # Docstring mod.write("#--") elif toktype == tokenize.COMMENT: # Comment mod.write("##\n") else: mod.write(ttext) prev_toktype = toktype last_col = ecol last_lineno = elineno if __name__ == '__main__': do_file(sys.argv[1]) 

    Estoy dejando comentarios de stub en lugar de cadenas de documentación y comentarios, ya que simplifica el código. Si los elimina por completo, también debe deshacerse de la muesca anterior.

    Soy el autor de ” mygod, él ha escrito un intérprete de python usando expresiones regulares … ” (es decir, un piraminificador) mencionado en el siguiente enlace =).
    Solo quería intervenir y decir que he mejorado el código bastante usando el módulo tokenizer (que descubrí gracias a esta pregunta =)).

    Te alegrará saber que el código ya no se basa tanto en expresiones regulares y usa el tokenizador con gran efecto. De todos modos, aquí está la función remove_comments_and_docstrings() de pyminifier
    (Nota: Funciona correctamente con los casos de borde en los que el código publicado anteriormente se rompe):

     import cStringIO, tokenize def remove_comments_and_docstrings(source): """ Returns 'source' minus comments and docstrings. """ io_obj = cStringIO.StringIO(source) out = "" prev_toktype = tokenize.INDENT last_lineno = -1 last_col = 0 for tok in tokenize.generate_tokens(io_obj.readline): token_type = tok[0] token_string = tok[1] start_line, start_col = tok[2] end_line, end_col = tok[3] ltext = tok[4] # The following two conditionals preserve indentation. # This is necessary because we're not using tokenize.untokenize() # (because it spits out code with copious amounts of oddly-placed # whitespace). if start_line > last_lineno: last_col = 0 if start_col > last_col: out += (" " * (start_col - last_col)) # Remove comments: if token_type == tokenize.COMMENT: pass # This series of conditionals removes docstrings: elif token_type == tokenize.STRING: if prev_toktype != tokenize.INDENT: # This is likely a docstring; double-check we're not inside an operator: if prev_toktype != tokenize.NEWLINE: # Note regarding NEWLINE vs NL: The tokenize module # differentiates between newlines that start a new statement # and newlines inside of operators such as parens, brackes, # and curly braces. Newlines inside of operators are # NEWLINE and newlines that start new code are NL. # Catch whole-module docstrings: if start_col > 0: # Unlabelled indentation means we're inside an operator out += token_string # Note regarding the INDENT token: The tokenize module does # not label indentation inside of an operator (parens, # brackets, and curly braces) as actual indentation. # For example: # def foo(): # "The spaces before this docstring are tokenize.INDENT" # test = [ # "The spaces before this string do not get a token" # ] else: out += token_string prev_toktype = token_type last_col = end_col last_lineno = end_line return out 

    Esta receta aquí pretende hacer lo que quieras. Y algunas otras cosas también.

    Intenta probar cada parte de los tokens que terminan con NEWLINE. Luego, el patrón correcto para la cadena de documentos (incluidos los casos en que sirve como comentario, pero no está asignado a __doc__ ) Creo que es (suponiendo que la coincidencia se realice desde el inicio del archivo después de NEWLINE):

     ( DEDENT+ | INDENT? ) STRING+ COMMENT? NEWLINE 

    Esto debería manejar todos los casos difíciles: concatenación de cadenas, continuación de línea, documentación de módulo / clase / función, comentarios en la línea de base después de la cadena. Tenga en cuenta que hay una diferencia entre los tokens NL y NEWLINE, por lo que no debemos preocuparnos por una sola cadena de la línea dentro de la expresión.

    Creo que la mejor manera es usar ast.

    Acabo de usar el código dado por Dan McDougall y he encontrado dos problemas.

    1. Había demasiadas líneas nuevas vacías, así que decidí eliminar la línea cada vez que tenemos dos líneas nuevas consecutivas
    2. Cuando se procesó el código de Python, faltaban todos los espacios (excepto la sangría) y cosas como “importar cualquier cosa” se convirtieron en “importAnything” que causó problemas. Agregué espacios después y antes de las palabras reservadas de Python que lo necesitaban. Espero no haber cometido ningún error allí.

    Creo que he arreglado ambas cosas agregando (antes de volver) algunas líneas más:

     # Removing unneeded newlines from string buffered_content = cStringIO.StringIO(content) # Takes the string generated by Dan McDougall's code as input content_without_newlines = "" previous_token_type = tokenize.NEWLINE for tokens in tokenize.generate_tokens(buffered_content.readline): token_type = tokens[0] token_string = tokens[1] if previous_token_type == tokenize.NL and token_type == tokenize.NL: pass else: # add necessary spaces prev_space = '' next_space = '' if token_string in ['and', 'as', 'or', 'in', 'is']: prev_space = ' ' if token_string in ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global', 'or', 'with', 'assert', 'if', 'yield', 'except', 'import', 'print', 'class', 'exec', 'in', 'raise', 'is', 'return', 'def', 'for', 'lambda']: next_space = ' ' content_without_newlines += prev_space + token_string + next_space # This will be our new output! previous_token_type = token_type