""" #################################################################################### Test: "python ...\Tools\visitor.py dir testmask [string]". Uses classes and subclasses to wrap some of the details of os.walk call usage to walk and search; testmask is an integer bitmask with 1 bit per available self-test; see also: visitor_*/.py subclasses use cases; frameworks should generally use__X pseudo private names, but all names here are exported for use in subclasses and clients; redefine reset to support multiple independent walks that require subclass updates; #################################################################################### """
import os, sys
class FileVisitor: """ Visits all nondirectory files below startDir (default '.'); override visit* methods to provide custom file/dir handlers; context arg/attribute is optional subclass-specific state; trace switch: 0 is silent, 1 is directories, 2 adds files """ def init(self, context=None, trace=2): self.fcount = 0 self.dcount = 0 self.context = context self.trace = trace
def run(self, startDir=os.curdir, reset=True):
if reset: self.reset()
for (thisDir, dirsHere, filesHere) in os.walk(startDir):
self.visitdir(thisDir)
for fname in filesHere: # for non-dir files
fpath = os.path.join(thisDir, fname) # fnames have no path
self.visitfile(fpath)
def reset(self): # to reuse walker
self.fcount = self.dcount = 0 # for independent walks
def visitdir(self, dirpath): # called for each dir
self.dcount += 1 # override or extend me
if self.trace > 0: print(dirpath, '...')
def visitfile(self, filepath): # called for each file
self.fcount += 1 # override or extend me
if self.trace > 1: print(self.fcount, '=>', filepath)
class SearchVisitor(FileVisitor): """ Search files at and below startDir for a string; subclass: redefine visitmatch, extension lists, candidate as needed; subclasses can use testexts to specify file types to search (but can also redefine candidate to use mimetypes for text content: see ahead) """
skipexts = []
testexts = ['.txt', '.py', '.pyw', '.html', '.c', '.h'] # search these exts
def __init__(self, searchkey, trace=2):
FileVisitor.__init__(self, searchkey, trace)
self.scount = 0
def reset(self): # on independent walks
self.scount = 0
def candidate(self, fname): # redef for mimetypes
ext = os.path.splitext(fname)[1]
if self.testexts:
return ext in self.testexts # in test list
else: # or not in skip list
return ext not in self.skipexts
def visitfile(self, fname): # test for a match
FileVisitor.visitfile(self, fname)
if not self.candidate(fname):
if self.trace > 0: print('Skipping', fname)
else:
text = open(fname).read() # 'rb' if undecodable
if self.context in text: # or text.find() != -1
self.visitmatch(fname, text)
self.scount += 1
def visitmatch(self, fname, text): # process a match
print('%s has %s' % (fname, self.context)) # override me lower
if name == 'main': # self-test logic dolist = 1 dosearch = 2 # 3=do list and search donext = 4 # when next test added
def selftest(testmask):
if testmask & dolist:
visitor = FileVisitor(trace=2)
visitor.run(sys.argv[2])
print('Visited %d files and %d dirs' % (visitor.fcount, visitor.dcount))
if testmask & dosearch:
visitor = SearchVisitor(sys.argv[3], trace=0)
visitor.run(sys.argv[2])
print('Found in %d files, visited %d' % (visitor.scount, visitor.fcount))
selftest(int(sys.argv[1])) # e.g., 3 = dolist | dosearch
""" Use: "python ...\Tools\visitor_edit.py string rootdir?". Add auto-editor startup to SearchVisitor in an external subclass component; Automatically pops up an editor on each file containing string as it traverses; can also use editor='edit' or 'notepad' on Windows; to use texteditor from later in the book, try r'python Gui\TextEditor\textEditor.py'; could also send a search command to go to the first match on start in some editors; """
import os, sys from visitor import SearchVisitor
class EditVisitor(SearchVisitor): """ edit files at and below startDir having string """ editor = r'C:\cygwin\bin\vim-nox.exe' # ymmv!
def visitmatch(self, fpathname, text):
os.system('%s %s' % (self.editor, fpathname))
if name == 'main': visitor = EditVisitor(sys.argv[1]) visitor.run('.' if len(sys.argv) < 3 else sys.argv[2]) print('Edited %d files, visited %d' % (visitor.scount, visitor.fcount))
""" Use: "python ...\Tools\visitor_replace.py rootdir fromStr toStr". Does global search-and-replace in all files in a directory tree: replaces fromStr with toStr in all text files; this is powerful but dangerous!! visitor_edit.py runs an editor for you to verify and make changes, and so is safer; use visitor_collect.py to simply collect matched files list; listonly mode here is similar to both SearchVisitor and CollectVisitor; """
import sys from visitor import SearchVisitor
class ReplaceVisitor(SearchVisitor): """ Change fromStr to toStr in files at and below startDir; files changed available in obj.changed list after a run """ def init(self, fromStr, toStr, listOnly=False, trace=0): self.changed = [] self.toStr = toStr self.listOnly = listOnly SearchVisitor.init(self, fromStr, trace)
def visitmatch(self, fname, text):
self.changed.append(fname)
if not self.listOnly:
fromStr, toStr = self.context, self.toStr
text = text.replace(fromStr, toStr)
open(fname, 'w').write(text)
if name == 'main': listonly = input('List only?') == 'y' visitor = ReplaceVisitor(sys.argv[2], sys.argv[3], listonly) if listonly or input('Proceed with changes?') == 'y': visitor.run(startDir=sys.argv[1]) action = 'Changed' if not listonly else 'Found' print('Visited %d files' % visitor.fcount) print(action, '%d files:' % len(visitor.changed)) for fname in visitor.changed: print(fname)