File: pyedit-grep-code.py
class TextEditor: def start(self): # run by GuiMaker.__init__ self.menuBar = [ # configure menu/toolbar ...etc... ('Search', 0, [('Goto...', 0, self.onGoto), ('Find...', 0, self.onFind), ('Refind', 0, self.onRefind), ('Change...', 0, self.onChange), ('Grep...', 3, self.onGrep)] ...etc... ...etc... def onGrep(self): """ new in version 2.1: threaded external file search; search matched filenames in directory tree for string; listbox clicks open matched file at line of occurrence; search is threaded so the GUI remains active and is not blocked, and to allow multiple greps to overlap in time; could use threadtools, but avoid loop if no active grep; grep Unicode policy: text files content in the searched tree might be in any Unicode encoding: we don't ask about each (as we do for opens), but allow the encoding used for the entire tree to be input, preset it to the platform filesystem or text default, and skip files that fail to decode; in worst cases, users may need to run grep N times if N encodings might exist; else opens may raise exceptions, and opening in binary mode might fail to match encoded text against search string; TBD: better to issue an error if any file fails to decode? but utf-16 2-bytes/char format created in Notepad may decode without error per utf-8, and search strings won't be found; TBD: could allow input of multiple encoding names, split on comma, try each one for every file, without open loadEncode? """ from PP4E.Gui.ShellGui.formrows import makeFormRow # nonmodal dialog: get dirnname, filenamepatt, grepkey popup = Toplevel() popup.title('PyEdit - grep') var1 = makeFormRow(popup, label='Directory root', width=18, browse=False) var2 = makeFormRow(popup, label='Filename pattern', width=18, browse=False) var3 = makeFormRow(popup, label='Search string', width=18, browse=False) var4 = makeFormRow(popup, label='Content encoding', width=18, browse=False) var1.set('.') # current dir var2.set('*.py') # initial values var4.set(sys.getdefaultencoding()) # for file content, not filenames cb = lambda: self.onDoGrep(var1.get(), var2.get(), var3.get(), var4.get()) Button(popup, text='Go',command=cb).pack() def onDoGrep(self, dirname, filenamepatt, grepkey, encoding): """ on Go in grep dialog: populate scrolled list with matches tbd: should producer thread be daemon so it dies with app? """ import threading, queue # make non-modal un-closeable dialog mypopup = Tk() mypopup.title('PyEdit - grepping') status = Label(mypopup, text='Grep thread searching for: %r...' % grepkey) status.pack(padx=20, pady=20) mypopup.protocol('WM_DELETE_WINDOW', lambda: None) # ignore X close # start producer thread, consumer loop myqueue = queue.Queue() threadargs = (filenamepatt, dirname, grepkey, encoding, myqueue) threading.Thread(target=self.grepThreadProducer, args=threadargs).start() self.grepThreadConsumer(grepkey, encoding, myqueue, mypopup) def grepThreadProducer(self, filenamepatt, dirname, grepkey, encoding, myqueue): """ in a non-GUI parallel thread: queue find.find results list; could also queue matches as found, but need to keep window; file content and file names may both fail to decode here; TBD: could pass encoded bytes to find() to avoid filename decoding excs in os.walk/listdir, but which encoding to use: sys.getfilesystemencoding() if not None? see also Chapter6 footnote issue: 3.1 fnmatch always converts bytes per Latin-1; """ from PP4E.Tools.find import find matches = [] try: for filepath in find(pattern=filenamepatt, startdir=dirname): try: textfile = open(filepath, encoding=encoding) for (linenum, linestr) in enumerate(textfile): if grepkey in linestr: msg = '%s@%d [%s]' % (filepath, linenum + 1, linestr) matches.append(msg) except UnicodeError as X: print('Unicode error in:', filepath, X) # eg: decode, bom except IOError as X: print('IO error in:', filepath, X) # eg: permission finally: myqueue.put(matches) # stop consumer loop on find excs: filenames? def grepThreadConsumer(self, grepkey, encoding, myqueue, mypopup): """ in the main GUI thread: watch queue for results or []; there may be multiple active grep threads/loops/queues; there may be other types of threads/checkers in process, especially when PyEdit is attached component (PyMailGUI); """ import queue try: matches = myqueue.get(block=False) except queue.Empty: myargs = (grepkey, encoding, myqueue, mypopup) self.after(250, self.grepThreadConsumer, *myargs) else: mypopup.destroy() # close status self.update() # erase it now if not matches: showinfo('PyEdit', 'Grep found no matches for: %r' % grepkey) else: self.grepMatchesList(matches, grepkey, encoding) def grepMatchesList(self, matches, grepkey, encoding): """ populate list after successful matches; we already know Unicode encoding from the search: use it here when filename clicked, so open doesn't ask user; """ from PP4E.Gui.Tour.scrolledlist import ScrolledList print('Matches for %s: %s' % (grepkey, len(matches))) # catch list double-click class ScrolledFilenames(ScrolledList): def runCommand(self, selection): file, line = selection.split(' [', 1)[0].split('@') editor = TextEditorMainPopup( loadFirst=file, winTitle=' grep match', loadEncode=encoding) editor.onGoto(int(line)) editor.text.focus_force() # no, really # new non-modal window popup = Tk() popup.title('PyEdit - grep matches: %r (%s)' % (grepkey, encoding)) ScrolledFilenames(parent=popup, options=matches) ...etc...