Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions preditor/gui/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import sys
from enum import Enum

import Qt as Qt_py
from Qt.QtCore import QRegExp, QSortFilterProxyModel, QStringListModel, Qt
from Qt.QtCore import QRegularExpression, QSortFilterProxyModel, QStringListModel, Qt
from Qt.QtGui import QCursor, QTextCursor
from Qt.QtWidgets import QCompleter, QToolTip

Expand Down Expand Up @@ -160,14 +159,12 @@ def refreshList(self, scope=None):
if self._completerMode == CompleterMode.FULL_FUZZY:
regExStr = ".*".join(prefix)

if Qt_py.IsPyQt6 or Qt_py.IsPySide6:
regexp = QRegExp(regExStr)
if self._sensitivity:
regexp.setPatternOptions(QRegExp.PatternOption.CaseInsensitiveOption)
self.filterModel.setFilterRegularExpression(regexp)
else:
regexp = QRegExp(regExStr, self._sensitivity)
self.filterModel.setFilterRegExp(regexp)
regexp = QRegularExpression(regExStr)
if self._sensitivity == Qt.CaseSensitivity.CaseInsensitive:
regexp.setPatternOptions(
QRegularExpression.PatternOption.CaseInsensitiveOption
)
self.filterModel.setFilterRegularExpression(regexp)

def clear(self):
self.popup().hide()
Expand Down
2 changes: 1 addition & 1 deletion preditor/gui/loggerwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ def connectSignals(self):

# Tooltips - Qt4 doesn't have a ToolTipsVisible method, so we fake it
regEx = ".*"
menus = self.findChildren(QtWidgets.QMenu, QtCore.QRegExp(regEx))
menus = self.findChildren(QtWidgets.QMenu, QtCore.QRegularExpression(regEx))
for menu in menus:
menu.hovered.connect(handleMenuHovered)

Expand Down
2 changes: 1 addition & 1 deletion preditor/gui/workboxwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ def searchText(self):
# refresh the search text unless we are using regular expressions
if (
not self._searchDialog.isVisible()
and not self._searchFlags & SearchOptions.QRegExp
and not self._searchFlags & SearchOptions.Regex
):
txt = self.selectedText()
if txt:
Expand Down
6 changes: 3 additions & 3 deletions preditor/scintilla/documenteditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class SearchOptions(EnumGroup):
Backward = Enum()
CaseSensitive = Enum()
WholeWords = Enum()
QRegExp = Enum()
Regex = Enum()


@contextmanager
Expand Down Expand Up @@ -649,7 +649,7 @@ def set_filename(self, filename):
self._filename = filename

def findNext(self, text, flags):
re = (flags & SearchOptions.QRegExp) != 0
re = (flags & SearchOptions.Regex) != 0
cs = (flags & SearchOptions.CaseSensitive) != 0
wo = (flags & SearchOptions.WholeWords) != 0
wrap = True
Expand All @@ -663,7 +663,7 @@ def findNext(self, text, flags):
return result

def findPrev(self, text, flags):
re = (flags & SearchOptions.QRegExp) != 0
re = (flags & SearchOptions.Regex) != 0
cs = (flags & SearchOptions.CaseSensitive) != 0
wo = (flags & SearchOptions.WholeWords) != 0
wrap = True
Expand Down
8 changes: 4 additions & 4 deletions preditor/scintilla/finddialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ def __init__(self, parent):
self.uiFindWholeWordsCHK.setChecked(
parent.searchFlags() & SearchOptions.WholeWords
)
self.uiQRegExpCHK.setChecked(parent.searchFlags() & SearchOptions.QRegExp)
self.uiRegexCHK.setChecked(parent.searchFlags() & SearchOptions.Regex)
self.uiSearchTXT.setPlainText(parent.searchText())

# update the signals
self.uiCaseSensitiveCHK.clicked.connect(self.updateSearchTerms)
self.uiFindWholeWordsCHK.clicked.connect(self.updateSearchTerms)
self.uiQRegExpCHK.clicked.connect(self.updateSearchTerms)
self.uiRegexCHK.clicked.connect(self.updateSearchTerms)
self.uiSearchTXT.textChanged.connect(self.updateSearchTerms)

self.uiFindNextBTN.clicked.connect(parent.uiFindNextACT.triggered.emit)
Expand Down Expand Up @@ -61,8 +61,8 @@ def updateSearchTerms(self):
options |= SearchOptions.CaseSensitive
if self.uiFindWholeWordsCHK.isChecked():
options |= SearchOptions.WholeWords
if self.uiQRegExpCHK.isChecked():
options |= SearchOptions.QRegExp
if self.uiRegexCHK.isChecked():
options |= SearchOptions.Regex

parent.setSearchFlags(options)
parent.setSearchText(self.uiSearchTXT.toPlainText())
10 changes: 5 additions & 5 deletions preditor/scintilla/ui/finddialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>436</width>
<width>421</width>
<height>170</height>
</rect>
</property>
Expand Down Expand Up @@ -115,12 +115,12 @@
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="uiQRegExpCHK">
<widget class="QCheckBox" name="uiRegexCHK">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Use Qt's QRegExp engine to find using regular expressions instead of a plain text find.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Find using regular expressions instead of plain text.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>QRegExp</string>
<string>Regex</string>
</property>
</widget>
</item>
Expand All @@ -135,7 +135,7 @@
<tabstop>uiFindPrevBTN</tabstop>
<tabstop>uiFindWholeWordsCHK</tabstop>
<tabstop>uiCaseSensitiveCHK</tabstop>
<tabstop>uiQRegExpCHK</tabstop>
<tabstop>uiRegexCHK</tabstop>
<tabstop>uiCloseBTN</tabstop>
</tabstops>
<resources/>
Expand Down
59 changes: 48 additions & 11 deletions preditor/stream/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@
from ..constants import StreamType


def _open_stream(stream):
"""Returns stream if it is still usable, otherwise None.

The old_stream a Director wraps may be closed before the Director is. For
example pytest closes its capture files at the end of a test run. Using a
closed stream raises ``ValueError: I/O operation on closed file``, so treat
a closed stream the same as not having an old_stream at all.

Note: It's possible the stream is closed after this call so its recommended to
use a try/except ValueError when accessing the returned stream.
"""
if stream is None:
return None
try:
if stream.closed:
return None
except (AttributeError, ValueError):
# Not all file like objects implement closed, assume it's usable.
pass
return stream


class _DirectorBuffer(io.RawIOBase):
"""Binary buffer that forwards text writes to the manager.

Expand All @@ -32,8 +54,9 @@ def __init__(self, manager, state, old_stream=None, name='nul'):
self.name = name

def flush(self):
if self.old_stream:
self.old_stream.flush()
old_stream = _open_stream(self.old_stream)
if old_stream:
old_stream.flush()
super().flush()

def writable(self):
Expand All @@ -47,8 +70,9 @@ def write(self, b):
msg = b.decode("utf-8", errors="replace")
self.manager.write(msg, self.state)

if self.old_stream:
self.old_stream.write(msg)
old_stream = _open_stream(self.old_stream)
if old_stream:
old_stream.write(msg)

return len(b)

Expand Down Expand Up @@ -115,7 +139,7 @@ def __repr__(self):

def close(self):
if (
self.old_stream
_open_stream(self.old_stream)
and not self.std_stream_wrapped
and self.old_stream is not sys.__stdout__
and self.old_stream is not sys.__stderr__
Expand All @@ -131,18 +155,31 @@ def write(self, msg):

# These methods enable terminal features like color coding etc.
def isatty(self):
if self.old_stream is not None:
return self.old_stream.isatty()
old_stream = _open_stream(self.old_stream)
if old_stream is not None:
try:
return old_stream.isatty()
except ValueError:
# The stream was closed after we checked it
return False
return False

@property
def encoding(self):
if self.old_stream is not None:
return self.old_stream.encoding
old_stream = _open_stream(self.old_stream)
if old_stream is not None:
try:
return old_stream.encoding
except ValueError:
pass
return super().encoding

@property
def errors(self):
if self.old_stream is not None:
return self.old_stream.errors
old_stream = _open_stream(self.old_stream)
if old_stream is not None:
try:
return old_stream.errors
except ValueError:
pass
return super().errors
25 changes: 25 additions & 0 deletions tests/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,28 @@ def test_newline_translation(manager):
else:
assert b"\r\n" not in result
assert result == b"Line 1\nLine 2\n"


def test_closed_old_stream(manager):
"""A closed old_stream is treated like there is no old_stream.

Something else may close old_stream before the Director is closed. For
example pytest closes its capture files at the end of a test run, but
colorama still checks `isatty` on the Director while shutting down.
"""
old_stream = io.StringIO()
director = Director(manager, "test_out", old_stream=old_stream)

old_stream.close()

assert director.isatty() is False
assert director.encoding == "utf-8"
assert director.errors == "strict"

# Writes are still recorded by the manager, they just aren't forwarded
director.write("Written to a closed stream")
director.flush()
assert manager.get_value() == "[test_out:Written to a closed stream]"

# Closing the Director doesn't raise on the already closed stream
director.close()