mirror of
https://github.com/python/cpython.git
synced 2025-08-17 15:21:26 +00:00

svn+ssh://pythondev@svn.python.org/python/trunk ................ r66801 | andrew.kuchling | 2008-10-04 23:51:59 +0200 (Sat, 04 Oct 2008) | 1 line Punctuation fix; expand dict.update docstring to be clearer ................ r66803 | benjamin.peterson | 2008-10-05 00:15:31 +0200 (Sun, 05 Oct 2008) | 1 line fix typo ................ r66804 | andrew.kuchling | 2008-10-05 02:11:56 +0200 (Sun, 05 Oct 2008) | 1 line #1415508 from Rocky Bernstein: add docstrings for enable_interspersed_args(), disable_interspersed_args() ................ r66813 | andrew.kuchling | 2008-10-06 14:07:04 +0200 (Mon, 06 Oct 2008) | 3 lines Per Greg Ward, optparse is no longer being externally maintained. I'll look at the bugs in the Optik bug tracker and copy them to the Python bug tracker if they're still relevant. ................ r66854 | georg.brandl | 2008-10-08 19:20:20 +0200 (Wed, 08 Oct 2008) | 2 lines #4059: patch up some sqlite docs. ................ r66855 | georg.brandl | 2008-10-08 19:30:55 +0200 (Wed, 08 Oct 2008) | 2 lines #4058: fix some whatsnew markup. ................ r66856 | georg.brandl | 2008-10-08 20:47:17 +0200 (Wed, 08 Oct 2008) | 3 lines #3935: properly support list subclasses in the C impl. of bisect. Patch reviewed by Raymond. ................ r66866 | benjamin.peterson | 2008-10-09 22:54:43 +0200 (Thu, 09 Oct 2008) | 1 line update paragraph about __future__ for 2.6 ................ r66870 | armin.rigo | 2008-10-10 10:40:44 +0200 (Fri, 10 Oct 2008) | 2 lines Typo: "ThreadError" is the name in the C source. ................ r66871 | benjamin.peterson | 2008-10-10 22:38:49 +0200 (Fri, 10 Oct 2008) | 1 line fix a small typo ................ r66872 | benjamin.peterson | 2008-10-10 22:51:37 +0200 (Fri, 10 Oct 2008) | 1 line talk about how you can unzip with zip ................ r66874 | benjamin.peterson | 2008-10-11 00:23:41 +0200 (Sat, 11 Oct 2008) | 1 line PyGILState_Acquire -> PyGILState_Ensure ................ r66887 | benjamin.peterson | 2008-10-13 23:51:40 +0200 (Mon, 13 Oct 2008) | 1 line document how to disable fixers ................ r66903 | benjamin.peterson | 2008-10-15 22:34:09 +0200 (Wed, 15 Oct 2008) | 1 line don't recurse into directories that start with '.' ................ r66905 | benjamin.peterson | 2008-10-15 23:05:55 +0200 (Wed, 15 Oct 2008) | 1 line support the optional line argument for idle ................ r66911 | benjamin.peterson | 2008-10-16 01:10:28 +0200 (Thu, 16 Oct 2008) | 41 lines Merged revisions 66805,66841,66860,66884-66886,66893,66907,66910 via svnmerge from svn+ssh://pythondev@svn.python.org/sandbox/trunk/2to3/lib2to3 ........ r66805 | benjamin.peterson | 2008-10-04 20:11:02 -0500 (Sat, 04 Oct 2008) | 1 line mention what the fixes directory is for ........ r66841 | benjamin.peterson | 2008-10-07 17:48:12 -0500 (Tue, 07 Oct 2008) | 1 line use assertFalse and assertTrue ........ r66860 | benjamin.peterson | 2008-10-08 16:05:07 -0500 (Wed, 08 Oct 2008) | 1 line instead of abusing the pattern matcher, use start_tree to find a next binding ........ r66884 | benjamin.peterson | 2008-10-13 15:50:30 -0500 (Mon, 13 Oct 2008) | 1 line don't print tokens to stdout when -v is given ........ r66885 | benjamin.peterson | 2008-10-13 16:28:57 -0500 (Mon, 13 Oct 2008) | 1 line add the -x option to disable fixers ........ r66886 | benjamin.peterson | 2008-10-13 16:33:53 -0500 (Mon, 13 Oct 2008) | 1 line cut down on some crud ........ r66893 | benjamin.peterson | 2008-10-14 17:16:54 -0500 (Tue, 14 Oct 2008) | 1 line add an optional set literal fixer ........ r66907 | benjamin.peterson | 2008-10-15 16:59:41 -0500 (Wed, 15 Oct 2008) | 1 line don't write backup files by default ........ r66910 | benjamin.peterson | 2008-10-15 17:43:10 -0500 (Wed, 15 Oct 2008) | 1 line add the -n option; it stops backupfiles from being written ........ ................ r66913 | benjamin.peterson | 2008-10-16 20:52:14 +0200 (Thu, 16 Oct 2008) | 1 line document that deque indexing is O(n) #4123 ................ r66927 | andrew.kuchling | 2008-10-16 22:15:47 +0200 (Thu, 16 Oct 2008) | 1 line Fix wording (2.6.1 backport candidate) ................ r66932 | benjamin.peterson | 2008-10-16 23:09:28 +0200 (Thu, 16 Oct 2008) | 1 line check for error conditions in _json #3623 ................ r66938 | benjamin.peterson | 2008-10-16 23:27:54 +0200 (Thu, 16 Oct 2008) | 1 line fix possible ref leak ................ r66942 | benjamin.peterson | 2008-10-16 23:48:06 +0200 (Thu, 16 Oct 2008) | 1 line fix more possible ref leaks in _json and use Py_CLEAR ................ r66962 | benjamin.peterson | 2008-10-17 22:01:01 +0200 (Fri, 17 Oct 2008) | 1 line clarify CALL_FUNCTION #4141 ................ r66964 | georg.brandl | 2008-10-17 23:41:49 +0200 (Fri, 17 Oct 2008) | 2 lines Fix duplicate word. ................ r66973 | armin.ronacher | 2008-10-19 10:27:43 +0200 (Sun, 19 Oct 2008) | 3 lines Fixed #4067 by implementing _attributes and _fields for the AST root node. ................ r66974 | benjamin.peterson | 2008-10-19 15:59:01 +0200 (Sun, 19 Oct 2008) | 1 line fix compiler warning ................ r66977 | benjamin.peterson | 2008-10-19 21:39:16 +0200 (Sun, 19 Oct 2008) | 1 line mention -n ................ r66992 | benjamin.peterson | 2008-10-21 22:51:13 +0200 (Tue, 21 Oct 2008) | 1 line make sure to call iteritems() ................ r66998 | benjamin.peterson | 2008-10-22 22:57:43 +0200 (Wed, 22 Oct 2008) | 1 line fix a few typos ................ r66999 | benjamin.peterson | 2008-10-22 23:05:30 +0200 (Wed, 22 Oct 2008) | 1 line and another typo... ................ r67002 | hirokazu.yamamoto | 2008-10-23 02:37:33 +0200 (Thu, 23 Oct 2008) | 1 line Issue #4183: Some tests didn't run with pickle.HIGHEST_PROTOCOL. ................ r67005 | walter.doerwald | 2008-10-23 15:11:39 +0200 (Thu, 23 Oct 2008) | 2 lines Use the correct names of the stateless codec functions (Fixes issue 4178). ................ r67007 | benjamin.peterson | 2008-10-23 23:43:48 +0200 (Thu, 23 Oct 2008) | 1 line only nonempty __slots__ don't work ................ r67028 | benjamin.peterson | 2008-10-26 01:27:07 +0200 (Sun, 26 Oct 2008) | 1 line don't use a catch-all ................ r67040 | armin.rigo | 2008-10-28 18:01:21 +0100 (Tue, 28 Oct 2008) | 5 lines Fix one of the tests: it relied on being present in an "output test" in order to actually test what it was supposed to test, i.e. that the code in the __del__ method did not crash. Use instead the new helper test_support.captured_output(). ................ r67041 | benjamin.peterson | 2008-10-29 21:33:00 +0100 (Wed, 29 Oct 2008) | 1 line mention the version gettempdir() was added ................ r67044 | amaury.forgeotdarc | 2008-10-30 00:15:57 +0100 (Thu, 30 Oct 2008) | 3 lines Correct error message in io.open(): closefd=True is the only accepted value with a file name. ................ r67070 | benjamin.peterson | 2008-10-31 21:41:44 +0100 (Fri, 31 Oct 2008) | 1 line rephrase has_key doc ................ r67089 | benjamin.peterson | 2008-11-03 21:43:20 +0100 (Mon, 03 Nov 2008) | 1 line clarify by splitting into multiple paragraphs ................ r67091 | benjamin.peterson | 2008-11-03 23:34:57 +0100 (Mon, 03 Nov 2008) | 1 line move a FileIO test to test_fileio ................ r67101 | georg.brandl | 2008-11-04 21:49:35 +0100 (Tue, 04 Nov 2008) | 2 lines #4167: fix markup glitches. ................ r67117 | georg.brandl | 2008-11-06 11:17:58 +0100 (Thu, 06 Nov 2008) | 2 lines #4268: Use correct module for two toplevel functions. ................ r67118 | georg.brandl | 2008-11-06 11:19:11 +0100 (Thu, 06 Nov 2008) | 2 lines #4267: small fixes in sqlite3 docs. ................ r67119 | georg.brandl | 2008-11-06 11:20:49 +0100 (Thu, 06 Nov 2008) | 2 lines #4245: move Thread section to the top. ................ r67123 | georg.brandl | 2008-11-06 19:49:15 +0100 (Thu, 06 Nov 2008) | 2 lines #4247: add "pass" examples to tutorial. ................ r67124 | andrew.kuchling | 2008-11-06 20:23:02 +0100 (Thu, 06 Nov 2008) | 1 line Fix grammar error; reword two paragraphs ................
521 lines
19 KiB
Python
Executable file
521 lines
19 KiB
Python
Executable file
#!/usr/bin/env python2.5
|
|
# Copyright 2006 Google, Inc. All Rights Reserved.
|
|
# Licensed to PSF under a Contributor Agreement.
|
|
|
|
"""Refactoring framework.
|
|
|
|
Used as a main program, this can refactor any number of files and/or
|
|
recursively descend down directories. Imported as a module, this
|
|
provides infrastructure to write your own refactoring tool.
|
|
"""
|
|
|
|
__author__ = "Guido van Rossum <guido@python.org>"
|
|
|
|
|
|
# Python imports
|
|
import os
|
|
import sys
|
|
import difflib
|
|
import logging
|
|
import operator
|
|
from collections import defaultdict
|
|
from itertools import chain
|
|
|
|
# Local imports
|
|
from .pgen2 import driver
|
|
from .pgen2 import tokenize
|
|
|
|
from . import pytree
|
|
from . import patcomp
|
|
from . import fixes
|
|
from . import pygram
|
|
|
|
|
|
def get_all_fix_names(fixer_pkg, remove_prefix=True):
|
|
"""Return a sorted list of all available fix names in the given package."""
|
|
pkg = __import__(fixer_pkg, [], [], ["*"])
|
|
fixer_dir = os.path.dirname(pkg.__file__)
|
|
fix_names = []
|
|
for name in sorted(os.listdir(fixer_dir)):
|
|
if name.startswith("fix_") and name.endswith(".py"):
|
|
if remove_prefix:
|
|
name = name[4:]
|
|
fix_names.append(name[:-3])
|
|
return fix_names
|
|
|
|
def get_head_types(pat):
|
|
""" Accepts a pytree Pattern Node and returns a set
|
|
of the pattern types which will match first. """
|
|
|
|
if isinstance(pat, (pytree.NodePattern, pytree.LeafPattern)):
|
|
# NodePatters must either have no type and no content
|
|
# or a type and content -- so they don't get any farther
|
|
# Always return leafs
|
|
return set([pat.type])
|
|
|
|
if isinstance(pat, pytree.NegatedPattern):
|
|
if pat.content:
|
|
return get_head_types(pat.content)
|
|
return set([None]) # Negated Patterns don't have a type
|
|
|
|
if isinstance(pat, pytree.WildcardPattern):
|
|
# Recurse on each node in content
|
|
r = set()
|
|
for p in pat.content:
|
|
for x in p:
|
|
r.update(get_head_types(x))
|
|
return r
|
|
|
|
raise Exception("Oh no! I don't understand pattern %s" %(pat))
|
|
|
|
def get_headnode_dict(fixer_list):
|
|
""" Accepts a list of fixers and returns a dictionary
|
|
of head node type --> fixer list. """
|
|
head_nodes = defaultdict(list)
|
|
for fixer in fixer_list:
|
|
if not fixer.pattern:
|
|
head_nodes[None].append(fixer)
|
|
continue
|
|
for t in get_head_types(fixer.pattern):
|
|
head_nodes[t].append(fixer)
|
|
return head_nodes
|
|
|
|
def get_fixers_from_package(pkg_name):
|
|
"""
|
|
Return the fully qualified names for fixers in the package pkg_name.
|
|
"""
|
|
return [pkg_name + "." + fix_name
|
|
for fix_name in get_all_fix_names(pkg_name, False)]
|
|
|
|
|
|
class FixerError(Exception):
|
|
"""A fixer could not be loaded."""
|
|
|
|
|
|
class RefactoringTool(object):
|
|
|
|
_default_options = {"print_function": False}
|
|
|
|
CLASS_PREFIX = "Fix" # The prefix for fixer classes
|
|
FILE_PREFIX = "fix_" # The prefix for modules with a fixer within
|
|
|
|
def __init__(self, fixer_names, options=None, explicit=None):
|
|
"""Initializer.
|
|
|
|
Args:
|
|
fixer_names: a list of fixers to import
|
|
options: an dict with configuration.
|
|
explicit: a list of fixers to run even if they are explicit.
|
|
"""
|
|
self.fixers = fixer_names
|
|
self.explicit = explicit or []
|
|
self.options = self._default_options.copy()
|
|
if options is not None:
|
|
self.options.update(options)
|
|
self.errors = []
|
|
self.logger = logging.getLogger("RefactoringTool")
|
|
self.fixer_log = []
|
|
self.wrote = False
|
|
if self.options["print_function"]:
|
|
del pygram.python_grammar.keywords["print"]
|
|
self.driver = driver.Driver(pygram.python_grammar,
|
|
convert=pytree.convert,
|
|
logger=self.logger)
|
|
self.pre_order, self.post_order = self.get_fixers()
|
|
|
|
self.pre_order = get_headnode_dict(self.pre_order)
|
|
self.post_order = get_headnode_dict(self.post_order)
|
|
|
|
self.files = [] # List of files that were or should be modified
|
|
|
|
def get_fixers(self):
|
|
"""Inspects the options to load the requested patterns and handlers.
|
|
|
|
Returns:
|
|
(pre_order, post_order), where pre_order is the list of fixers that
|
|
want a pre-order AST traversal, and post_order is the list that want
|
|
post-order traversal.
|
|
"""
|
|
pre_order_fixers = []
|
|
post_order_fixers = []
|
|
for fix_mod_path in self.fixers:
|
|
mod = __import__(fix_mod_path, {}, {}, ["*"])
|
|
fix_name = fix_mod_path.rsplit(".", 1)[-1]
|
|
if fix_name.startswith(self.FILE_PREFIX):
|
|
fix_name = fix_name[len(self.FILE_PREFIX):]
|
|
parts = fix_name.split("_")
|
|
class_name = self.CLASS_PREFIX + "".join([p.title() for p in parts])
|
|
try:
|
|
fix_class = getattr(mod, class_name)
|
|
except AttributeError:
|
|
raise FixerError("Can't find %s.%s" % (fix_name, class_name))
|
|
fixer = fix_class(self.options, self.fixer_log)
|
|
if fixer.explicit and self.explicit is not True and \
|
|
fix_mod_path not in self.explicit:
|
|
self.log_message("Skipping implicit fixer: %s", fix_name)
|
|
continue
|
|
|
|
self.log_debug("Adding transformation: %s", fix_name)
|
|
if fixer.order == "pre":
|
|
pre_order_fixers.append(fixer)
|
|
elif fixer.order == "post":
|
|
post_order_fixers.append(fixer)
|
|
else:
|
|
raise FixerError("Illegal fixer order: %r" % fixer.order)
|
|
|
|
key_func = operator.attrgetter("run_order")
|
|
pre_order_fixers.sort(key=key_func)
|
|
post_order_fixers.sort(key=key_func)
|
|
return (pre_order_fixers, post_order_fixers)
|
|
|
|
def log_error(self, msg, *args, **kwds):
|
|
"""Called when an error occurs."""
|
|
raise
|
|
|
|
def log_message(self, msg, *args):
|
|
"""Hook to log a message."""
|
|
if args:
|
|
msg = msg % args
|
|
self.logger.info(msg)
|
|
|
|
def log_debug(self, msg, *args):
|
|
if args:
|
|
msg = msg % args
|
|
self.logger.debug(msg)
|
|
|
|
def print_output(self, lines):
|
|
"""Called with lines of output to give to the user."""
|
|
pass
|
|
|
|
def refactor(self, items, write=False, doctests_only=False):
|
|
"""Refactor a list of files and directories."""
|
|
for dir_or_file in items:
|
|
if os.path.isdir(dir_or_file):
|
|
self.refactor_dir(dir_or_file, write, doctests_only)
|
|
else:
|
|
self.refactor_file(dir_or_file, write, doctests_only)
|
|
|
|
def refactor_dir(self, dir_name, write=False, doctests_only=False):
|
|
"""Descends down a directory and refactor every Python file found.
|
|
|
|
Python files are assumed to have a .py extension.
|
|
|
|
Files and subdirectories starting with '.' are skipped.
|
|
"""
|
|
for dirpath, dirnames, filenames in os.walk(dir_name):
|
|
self.log_debug("Descending into %s", dirpath)
|
|
dirnames.sort()
|
|
filenames.sort()
|
|
for name in filenames:
|
|
if not name.startswith(".") and name.endswith("py"):
|
|
fullname = os.path.join(dirpath, name)
|
|
self.refactor_file(fullname, write, doctests_only)
|
|
# Modify dirnames in-place to remove subdirs with leading dots
|
|
dirnames[:] = [dn for dn in dirnames if not dn.startswith(".")]
|
|
|
|
def refactor_file(self, filename, write=False, doctests_only=False):
|
|
"""Refactors a file."""
|
|
try:
|
|
f = open(filename)
|
|
except IOError, err:
|
|
self.log_error("Can't open %s: %s", filename, err)
|
|
return
|
|
try:
|
|
input = f.read() + "\n" # Silence certain parse errors
|
|
finally:
|
|
f.close()
|
|
if doctests_only:
|
|
self.log_debug("Refactoring doctests in %s", filename)
|
|
output = self.refactor_docstring(input, filename)
|
|
if output != input:
|
|
self.processed_file(output, filename, input, write=write)
|
|
else:
|
|
self.log_debug("No doctest changes in %s", filename)
|
|
else:
|
|
tree = self.refactor_string(input, filename)
|
|
if tree and tree.was_changed:
|
|
# The [:-1] is to take off the \n we added earlier
|
|
self.processed_file(str(tree)[:-1], filename, write=write)
|
|
else:
|
|
self.log_debug("No changes in %s", filename)
|
|
|
|
def refactor_string(self, data, name):
|
|
"""Refactor a given input string.
|
|
|
|
Args:
|
|
data: a string holding the code to be refactored.
|
|
name: a human-readable name for use in error/log messages.
|
|
|
|
Returns:
|
|
An AST corresponding to the refactored input stream; None if
|
|
there were errors during the parse.
|
|
"""
|
|
try:
|
|
tree = self.driver.parse_string(data)
|
|
except Exception, err:
|
|
self.log_error("Can't parse %s: %s: %s",
|
|
name, err.__class__.__name__, err)
|
|
return
|
|
self.log_debug("Refactoring %s", name)
|
|
self.refactor_tree(tree, name)
|
|
return tree
|
|
|
|
def refactor_stdin(self, doctests_only=False):
|
|
input = sys.stdin.read()
|
|
if doctests_only:
|
|
self.log_debug("Refactoring doctests in stdin")
|
|
output = self.refactor_docstring(input, "<stdin>")
|
|
if output != input:
|
|
self.processed_file(output, "<stdin>", input)
|
|
else:
|
|
self.log_debug("No doctest changes in stdin")
|
|
else:
|
|
tree = self.refactor_string(input, "<stdin>")
|
|
if tree and tree.was_changed:
|
|
self.processed_file(str(tree), "<stdin>", input)
|
|
else:
|
|
self.log_debug("No changes in stdin")
|
|
|
|
def refactor_tree(self, tree, name):
|
|
"""Refactors a parse tree (modifying the tree in place).
|
|
|
|
Args:
|
|
tree: a pytree.Node instance representing the root of the tree
|
|
to be refactored.
|
|
name: a human-readable name for this tree.
|
|
|
|
Returns:
|
|
True if the tree was modified, False otherwise.
|
|
"""
|
|
# Two calls to chain are required because pre_order.values()
|
|
# will be a list of lists of fixers:
|
|
# [[<fixer ...>, <fixer ...>], [<fixer ...>]]
|
|
all_fixers = chain(chain(*self.pre_order.values()),\
|
|
chain(*self.post_order.values()))
|
|
for fixer in all_fixers:
|
|
fixer.start_tree(tree, name)
|
|
|
|
self.traverse_by(self.pre_order, tree.pre_order())
|
|
self.traverse_by(self.post_order, tree.post_order())
|
|
|
|
for fixer in all_fixers:
|
|
fixer.finish_tree(tree, name)
|
|
return tree.was_changed
|
|
|
|
def traverse_by(self, fixers, traversal):
|
|
"""Traverse an AST, applying a set of fixers to each node.
|
|
|
|
This is a helper method for refactor_tree().
|
|
|
|
Args:
|
|
fixers: a list of fixer instances.
|
|
traversal: a generator that yields AST nodes.
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
if not fixers:
|
|
return
|
|
for node in traversal:
|
|
for fixer in fixers[node.type] + fixers[None]:
|
|
results = fixer.match(node)
|
|
if results:
|
|
new = fixer.transform(node, results)
|
|
if new is not None and (new != node or
|
|
str(new) != str(node)):
|
|
node.replace(new)
|
|
node = new
|
|
|
|
def processed_file(self, new_text, filename, old_text=None, write=False):
|
|
"""
|
|
Called when a file has been refactored, and there are changes.
|
|
"""
|
|
self.files.append(filename)
|
|
if old_text is None:
|
|
try:
|
|
f = open(filename, "r")
|
|
except IOError, err:
|
|
self.log_error("Can't read %s: %s", filename, err)
|
|
return
|
|
try:
|
|
old_text = f.read()
|
|
finally:
|
|
f.close()
|
|
if old_text == new_text:
|
|
self.log_debug("No changes to %s", filename)
|
|
return
|
|
self.print_output(diff_texts(old_text, new_text, filename))
|
|
if write:
|
|
self.write_file(new_text, filename, old_text)
|
|
else:
|
|
self.log_debug("Not writing changes to %s", filename)
|
|
|
|
def write_file(self, new_text, filename, old_text):
|
|
"""Writes a string to a file.
|
|
|
|
It first shows a unified diff between the old text and the new text, and
|
|
then rewrites the file; the latter is only done if the write option is
|
|
set.
|
|
"""
|
|
try:
|
|
f = open(filename, "w")
|
|
except os.error, err:
|
|
self.log_error("Can't create %s: %s", filename, err)
|
|
return
|
|
try:
|
|
try:
|
|
f.write(new_text)
|
|
except os.error, err:
|
|
self.log_error("Can't write %s: %s", filename, err)
|
|
finally:
|
|
f.close()
|
|
self.log_debug("Wrote changes to %s", filename)
|
|
self.wrote = True
|
|
|
|
PS1 = ">>> "
|
|
PS2 = "... "
|
|
|
|
def refactor_docstring(self, input, filename):
|
|
"""Refactors a docstring, looking for doctests.
|
|
|
|
This returns a modified version of the input string. It looks
|
|
for doctests, which start with a ">>>" prompt, and may be
|
|
continued with "..." prompts, as long as the "..." is indented
|
|
the same as the ">>>".
|
|
|
|
(Unfortunately we can't use the doctest module's parser,
|
|
since, like most parsers, it is not geared towards preserving
|
|
the original source.)
|
|
"""
|
|
result = []
|
|
block = None
|
|
block_lineno = None
|
|
indent = None
|
|
lineno = 0
|
|
for line in input.splitlines(True):
|
|
lineno += 1
|
|
if line.lstrip().startswith(self.PS1):
|
|
if block is not None:
|
|
result.extend(self.refactor_doctest(block, block_lineno,
|
|
indent, filename))
|
|
block_lineno = lineno
|
|
block = [line]
|
|
i = line.find(self.PS1)
|
|
indent = line[:i]
|
|
elif (indent is not None and
|
|
(line.startswith(indent + self.PS2) or
|
|
line == indent + self.PS2.rstrip() + "\n")):
|
|
block.append(line)
|
|
else:
|
|
if block is not None:
|
|
result.extend(self.refactor_doctest(block, block_lineno,
|
|
indent, filename))
|
|
block = None
|
|
indent = None
|
|
result.append(line)
|
|
if block is not None:
|
|
result.extend(self.refactor_doctest(block, block_lineno,
|
|
indent, filename))
|
|
return "".join(result)
|
|
|
|
def refactor_doctest(self, block, lineno, indent, filename):
|
|
"""Refactors one doctest.
|
|
|
|
A doctest is given as a block of lines, the first of which starts
|
|
with ">>>" (possibly indented), while the remaining lines start
|
|
with "..." (identically indented).
|
|
|
|
"""
|
|
try:
|
|
tree = self.parse_block(block, lineno, indent)
|
|
except Exception, err:
|
|
if self.log.isEnabledFor(logging.DEBUG):
|
|
for line in block:
|
|
self.log_debug("Source: %s", line.rstrip("\n"))
|
|
self.log_error("Can't parse docstring in %s line %s: %s: %s",
|
|
filename, lineno, err.__class__.__name__, err)
|
|
return block
|
|
if self.refactor_tree(tree, filename):
|
|
new = str(tree).splitlines(True)
|
|
# Undo the adjustment of the line numbers in wrap_toks() below.
|
|
clipped, new = new[:lineno-1], new[lineno-1:]
|
|
assert clipped == ["\n"] * (lineno-1), clipped
|
|
if not new[-1].endswith("\n"):
|
|
new[-1] += "\n"
|
|
block = [indent + self.PS1 + new.pop(0)]
|
|
if new:
|
|
block += [indent + self.PS2 + line for line in new]
|
|
return block
|
|
|
|
def summarize(self):
|
|
if self.wrote:
|
|
were = "were"
|
|
else:
|
|
were = "need to be"
|
|
if not self.files:
|
|
self.log_message("No files %s modified.", were)
|
|
else:
|
|
self.log_message("Files that %s modified:", were)
|
|
for file in self.files:
|
|
self.log_message(file)
|
|
if self.fixer_log:
|
|
self.log_message("Warnings/messages while refactoring:")
|
|
for message in self.fixer_log:
|
|
self.log_message(message)
|
|
if self.errors:
|
|
if len(self.errors) == 1:
|
|
self.log_message("There was 1 error:")
|
|
else:
|
|
self.log_message("There were %d errors:", len(self.errors))
|
|
for msg, args, kwds in self.errors:
|
|
self.log_message(msg, *args, **kwds)
|
|
|
|
def parse_block(self, block, lineno, indent):
|
|
"""Parses a block into a tree.
|
|
|
|
This is necessary to get correct line number / offset information
|
|
in the parser diagnostics and embedded into the parse tree.
|
|
"""
|
|
return self.driver.parse_tokens(self.wrap_toks(block, lineno, indent))
|
|
|
|
def wrap_toks(self, block, lineno, indent):
|
|
"""Wraps a tokenize stream to systematically modify start/end."""
|
|
tokens = tokenize.generate_tokens(self.gen_lines(block, indent).next)
|
|
for type, value, (line0, col0), (line1, col1), line_text in tokens:
|
|
line0 += lineno - 1
|
|
line1 += lineno - 1
|
|
# Don't bother updating the columns; this is too complicated
|
|
# since line_text would also have to be updated and it would
|
|
# still break for tokens spanning lines. Let the user guess
|
|
# that the column numbers for doctests are relative to the
|
|
# end of the prompt string (PS1 or PS2).
|
|
yield type, value, (line0, col0), (line1, col1), line_text
|
|
|
|
|
|
def gen_lines(self, block, indent):
|
|
"""Generates lines as expected by tokenize from a list of lines.
|
|
|
|
This strips the first len(indent + self.PS1) characters off each line.
|
|
"""
|
|
prefix1 = indent + self.PS1
|
|
prefix2 = indent + self.PS2
|
|
prefix = prefix1
|
|
for line in block:
|
|
if line.startswith(prefix):
|
|
yield line[len(prefix):]
|
|
elif line == prefix.rstrip() + "\n":
|
|
yield "\n"
|
|
else:
|
|
raise AssertionError("line=%r, prefix=%r" % (line, prefix))
|
|
prefix = prefix2
|
|
while True:
|
|
yield ""
|
|
|
|
|
|
def diff_texts(a, b, filename):
|
|
"""Return a unified diff of two strings."""
|
|
a = a.splitlines()
|
|
b = b.splitlines()
|
|
return difflib.unified_diff(a, b, filename, filename,
|
|
"(original)", "(refactored)",
|
|
lineterm="")
|