Thread View
j: Next unread message
k: Previous unread message
j a: Jump to all threads
j l: Jump to MailingList overview
http://hg.python.org/cpython/rev/4d604f1f0219
changeset: 85482:4d604f1f0219
user: Eli Bendersky <eliben(a)gmail.com>
date: Sat Aug 31 15:18:48 2013 -0700
summary:
Update whatsnew/3.4.rst wrt. the socket constants switch to IntEnum
[issue #18730]
files:
Doc/whatsnew/3.4.rst | 3 +++
1 files changed, 3 insertions(+), 0 deletions(-)
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst
--- a/Doc/whatsnew/3.4.rst
+++ b/Doc/whatsnew/3.4.rst
@@ -320,6 +320,9 @@
* :meth:`socket.socket.get_inheritable`, :meth:`socket.socket.set_inheritable`
+The ``socket.AF_*`` and ``socket.SOCK_*`` constants are enumeration values,
+using the new :mod:`enum` module. This allows descriptive reporting during
+debugging, instead of seeing integer "magic numbers".
ssl
---
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/038543d34166
changeset: 85481:038543d34166
user: Eli Bendersky <eliben(a)gmail.com>
date: Sat Aug 31 15:13:30 2013 -0700
summary:
Switch the AF_* and SOCK_* constants in the socket module to IntEnum.
Closes #18720.
files:
Lib/socket.py | 66 ++++++++++++++++++++++++++++-
Lib/test/test_socket.py | 28 +++++++++++-
2 files changed, 91 insertions(+), 3 deletions(-)
diff --git a/Lib/socket.py b/Lib/socket.py
--- a/Lib/socket.py
+++ b/Lib/socket.py
@@ -48,6 +48,7 @@
from _socket import *
import os, sys, io
+from enum import IntEnum
try:
import errno
@@ -60,6 +61,30 @@
__all__ = ["getfqdn", "create_connection"]
__all__.extend(os._get_exports_list(_socket))
+# Set up the socket.AF_* socket.SOCK_* constants as members of IntEnums for
+# nicer string representations.
+# Note that _socket only knows about the integer values. The public interface
+# in this module understands the enums and translates them back from integers
+# where needed (e.g. .family property of a socket object).
+AddressFamily = IntEnum('AddressFamily',
+ {name: value for name, value in globals().items()
+ if name.isupper() and name.startswith('AF_')})
+globals().update(AddressFamily.__members__)
+
+SocketType = IntEnum('SocketType',
+ {name: value for name, value in globals().items()
+ if name.isupper() and name.startswith('SOCK_')})
+globals().update(SocketType.__members__)
+
+def _intenum_converter(value, enum_klass):
+ """Convert a numeric family value to an IntEnum member.
+
+ If it's not a known member, return the numeric value itself.
+ """
+ try:
+ return enum_klass(value)
+ except ValueError:
+ return value
_realsocket = socket
@@ -91,6 +116,10 @@
__slots__ = ["__weakref__", "_io_refs", "_closed"]
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None):
+ # For user code address family and type values are IntEnum members, but
+ # for the underlying _socket.socket they're just integers. The
+ # constructor of _socket.socket converts the given argument to an
+ # integer automatically.
_socket.socket.__init__(self, family, type, proto, fileno)
self._io_refs = 0
self._closed = False
@@ -230,6 +259,18 @@
self._closed = True
return super().detach()
+ @property
+ def family(self):
+ """Read-only access to the address family for this socket.
+ """
+ return _intenum_converter(super().family, AddressFamily)
+
+ @property
+ def type(self):
+ """Read-only access to the socket type.
+ """
+ return _intenum_converter(super().type, SocketType)
+
if os.name == 'nt':
def get_inheritable(self):
return os.get_handle_inheritable(self.fileno())
@@ -243,7 +284,6 @@
get_inheritable.__doc__ = "Get the inheritable flag of the socket"
set_inheritable.__doc__ = "Set the inheritable flag of the socket"
-
def fromfd(fd, family, type, proto=0):
""" fromfd(fd, family, type[, proto]) -> socket object
@@ -469,3 +509,27 @@
raise err
else:
raise error("getaddrinfo returns an empty list")
+
+def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
+ """Resolve host and port into list of address info entries.
+
+ Translate the host/port argument into a sequence of 5-tuples that contain
+ all the necessary arguments for creating a socket connected to that service.
+ host is a domain name, a string representation of an IPv4/v6 address or
+ None. port is a string service name such as 'http', a numeric port number or
+ None. By passing None as the value of host and port, you can pass NULL to
+ the underlying C API.
+
+ The family, type and proto arguments can be optionally specified in order to
+ narrow the list of addresses returned. Passing zero as a value for each of
+ these arguments selects the full range of results.
+ """
+ # We override this function since we want to translate the numeric family
+ # and socket type values to enum constants.
+ addrlist = []
+ for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
+ af, socktype, proto, canonname, sa = res
+ addrlist.append((_intenum_converter(af, AddressFamily),
+ _intenum_converter(socktype, SocketType),
+ proto, canonname, sa))
+ return addrlist
diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py
--- a/Lib/test/test_socket.py
+++ b/Lib/test/test_socket.py
@@ -1161,9 +1161,12 @@
socket.getaddrinfo(HOST, 80)
socket.getaddrinfo(HOST, None)
# test family and socktype filters
- infos = socket.getaddrinfo(HOST, None, socket.AF_INET)
- for family, _, _, _, _ in infos:
+ infos = socket.getaddrinfo(HOST, 80, socket.AF_INET, socket.SOCK_STREAM)
+ for family, type, _, _, _ in infos:
self.assertEqual(family, socket.AF_INET)
+ self.assertEqual(str(family), 'AddressFamily.AF_INET')
+ self.assertEqual(type, socket.SOCK_STREAM)
+ self.assertEqual(str(type), 'SocketType.SOCK_STREAM')
infos = socket.getaddrinfo(HOST, None, 0, socket.SOCK_STREAM)
for _, socktype, _, _, _ in infos:
self.assertEqual(socktype, socket.SOCK_STREAM)
@@ -1321,6 +1324,27 @@
with socket.socket(socket.AF_INET6, socket.SOCK_STREAM) as s:
self.assertRaises(OverflowError, s.bind, (support.HOSTv6, 0, -10))
+ def test_str_for_enums(self):
+ # Make sure that the AF_* and SOCK_* constants have enum-like string
+ # reprs.
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ self.assertEqual(str(s.family), 'AddressFamily.AF_INET')
+ self.assertEqual(str(s.type), 'SocketType.SOCK_STREAM')
+
+ @unittest.skipIf(os.name == 'nt', 'Will not work on Windows')
+ def test_uknown_socket_family_repr(self):
+ # Test that when created with a family that's not one of the known
+ # AF_*/SOCK_* constants, socket.family just returns the number.
+ #
+ # To do this we fool socket.socket into believing it already has an
+ # open fd because on this path it doesn't actually verify the family and
+ # type and populates the socket object.
+ #
+ # On Windows this trick won't work, so the test is skipped.
+ fd, _ = tempfile.mkstemp()
+ with socket.socket(family=42424, type=13331, fileno=fd) as s:
+ self.assertEqual(s.family, 42424)
+ self.assertEqual(s.type, 13331)
@unittest.skipUnless(HAVE_SOCKET_CAN, 'SocketCan required for this test.')
class BasicCANTest(unittest.TestCase):
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/5600e9a5c35d
changeset: 85480:5600e9a5c35d
parent: 85478:7605847c15a4
parent: 85479:f0eedca4b2a2
user: Terry Jan Reedy <tjreedy(a)udel.edu>
date: Sat Aug 31 17:16:45 2013 -0400
summary:
Issue #12037: Fix test_email for desktop Windows.
files:
Lib/test/test_email/test_email.py | 8 ++++----
Misc/NEWS | 2 ++
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -180,8 +180,8 @@
def test_byte_message_rfc822_only(self):
# Make sure new bytes header parser also passes this.
- with openfile('msg_46.txt', 'rb') as fp:
- msgdata = fp.read()
+ with openfile('msg_46.txt') as fp:
+ msgdata = fp.read().encode('ascii')
parser = email.parser.BytesHeaderParser()
msg = parser.parsebytes(msgdata)
out = BytesIO()
@@ -269,8 +269,8 @@
def test_as_bytes(self):
msg = self._msgobj('msg_01.txt')
- with openfile('msg_01.txt', 'rb') as fp:
- data = fp.read()
+ with openfile('msg_01.txt') as fp:
+ data = fp.read().encode('ascii')
self.assertEqual(data, bytes(msg))
fullrepr = msg.as_bytes(unixfrom=True)
lines = fullrepr.split(b'\n')
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -165,6 +165,8 @@
Tests
-----
+- Issue #12037: Fix test_email for desktop Windows.
+
- Issue #15507: test_subprocess's test_send_signal could fail if the test
runner were run in an environment where the process inherited an ignore
setting for SIGINT. Restore the SIGINT handler to the desired
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/f0eedca4b2a2
changeset: 85479:f0eedca4b2a2
branch: 3.3
parent: 85477:dfbf0f9034cc
user: Terry Jan Reedy <tjreedy(a)udel.edu>
date: Sat Aug 31 17:12:21 2013 -0400
summary:
Issue #12037: Fix test_email for desktop Windows.
files:
Lib/test/test_email/test_email.py | 4 ++--
Misc/NEWS | 2 ++
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -180,8 +180,8 @@
def test_byte_message_rfc822_only(self):
# Make sure new bytes header parser also passes this.
- with openfile('msg_46.txt', 'rb') as fp:
- msgdata = fp.read()
+ with openfile('msg_46.txt') as fp:
+ msgdata = fp.read().encode('ascii')
parser = email.parser.BytesHeaderParser()
msg = parser.parsebytes(msgdata)
out = BytesIO()
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -314,6 +314,8 @@
Tests
-----
+- Issue #12037: Fix test_email for desktop Windows.
+
- Issue #15507: test_subprocess's test_send_signal could fail if the test
runner were run in an environment where the process inherited an ignore
setting for SIGINT. Restore the SIGINT handler to the desired
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/7605847c15a4
changeset: 85478:7605847c15a4
parent: 85475:833246d42825
parent: 85477:dfbf0f9034cc
user: Terry Jan Reedy <tjreedy(a)udel.edu>
date: Sat Aug 31 16:28:53 2013 -0400
summary:
Merge from 3.3 #18489 Search Engine tests
files:
Lib/idlelib/SearchEngine.py | 72 +-
Lib/idlelib/idle_test/test_searchengine.py | 326 ++++++++++
Misc/NEWS | 5 +
3 files changed, 369 insertions(+), 34 deletions(-)
diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py
--- a/Lib/idlelib/SearchEngine.py
+++ b/Lib/idlelib/SearchEngine.py
@@ -1,6 +1,6 @@
'''Define SearchEngine for search dialogs.'''
import re
-from tkinter import *
+from tkinter import StringVar, BooleanVar, TclError
import tkinter.messagebox as tkMessageBox
def get(root):
@@ -22,14 +22,13 @@
The dialogs bind these to the UI elements present in the dialogs.
'''
- self.root = root
- self.patvar = StringVar(root) # search pattern
- self.revar = BooleanVar(root) # regular expression?
- self.casevar = BooleanVar(root) # match case?
- self.wordvar = BooleanVar(root) # match whole word?
- self.wrapvar = BooleanVar(root) # wrap around buffer?
- self.wrapvar.set(1) # (on by default)
- self.backvar = BooleanVar(root) # search backwards?
+ self.root = root # need for report_error()
+ self.patvar = StringVar(root, '') # search pattern
+ self.revar = BooleanVar(root, False) # regular expression?
+ self.casevar = BooleanVar(root, False) # match case?
+ self.wordvar = BooleanVar(root, False) # match whole word?
+ self.wrapvar = BooleanVar(root, True) # wrap around buffer?
+ self.backvar = BooleanVar(root, False) # search backwards?
# Access methods
@@ -56,9 +55,16 @@
# Higher level access methods
+ def setcookedpat(self, pat):
+ "Set pattern after escaping if re."
+ # called only in SearchDialog.py: 66
+ if self.isre():
+ pat = re.escape(pat)
+ self.setpat(pat)
+
def getcookedpat(self):
pat = self.getpat()
- if not self.isre():
+ if not self.isre(): # if True, see setcookedpat
pat = re.escape(pat)
if self.isword():
pat = r"\b%s\b" % pat
@@ -90,33 +96,28 @@
# Derived class could override this with something fancier
msg = "Error: " + str(msg)
if pat:
- msg = msg + "\np\Pattern: " + str(pat)
+ msg = msg + "\nPattern: " + str(pat)
if col >= 0:
msg = msg + "\nOffset: " + str(col)
tkMessageBox.showerror("Regular expression error",
msg, master=self.root)
- def setcookedpat(self, pat):
- if self.isre():
- pat = re.escape(pat)
- self.setpat(pat)
+ def search_text(self, text, prog=None, ok=0):
+ '''Return (lineno, matchobj) or None for forward/backward search.
- def search_text(self, text, prog=None, ok=0):
- '''Return (lineno, matchobj) for prog in text widget, or None.
+ This function calls the right function with the right arguments.
+ It directly return the result of that call.
- If prog is given, it should be a precompiled pattern.
- Wrap (yes/no) and direction (forward/back) settings are used.
+ Text is a text widget. Prog is a precompiled pattern.
+ The ok parameteris a bit complicated as it has two effects.
- The search starts at the selection (if there is one) or at the
- insert mark (otherwise). If the search is forward, it starts
- at the right of the selection; for a backward search, it
- starts at the left end. An empty match exactly at either end
- of the selection (or at the insert mark if there is no
- selection) is ignored unless the ok flag is true -- this is
- done to guarantee progress.
+ If there is a selection, the search begin at either end,
+ depending on the direction setting and ok, with ok meaning that
+ the search starts with the selection. Otherwise, search begins
+ at the insert mark.
- If the search is allowed to wrap around, it will return the
- original selection if (and only if) it is the only match.
+ To aid progress, the search functions do not return an empty
+ match at the starting position unless ok is True.
'''
if not prog:
@@ -188,15 +189,18 @@
return None
def search_reverse(prog, chars, col):
- '''Search backwards in a string (line of text).
+ '''Search backwards and return an re match object or None.
This is done by searching forwards until there is no match.
+ Prog: compiled re object with a search method returning a match.
+ Chars: line of text, without \n.
+ Col: stop index for the search; the limit for match.end().
'''
m = prog.search(chars)
if not m:
return None
found = None
- i, j = m.span()
+ i, j = m.span() # m.start(), m.end() == match slice indexes
while i < col and j <= col:
found = m
if i == j:
@@ -226,7 +230,7 @@
line, col = map(int, index.split(".")) # Fails on invalid index
return line, col
-##if __name__ == "__main__":
-## from test import support; support.use_resources = ['gui']
-## import unittest
-## unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
+if __name__ == "__main__":
+ from test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
new file mode 100644
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_searchengine.py
@@ -0,0 +1,326 @@
+'''Test functions and SearchEngine class in SearchEngine.py.'''
+
+# With mock replacements, the module does not use any gui widgets.
+# The use of tk.Text is avoided (for now, until mock Text is improved)
+# by patching instances with an index function returning what is needed.
+# This works because mock Text.get does not use .index.
+
+import re
+import unittest
+from test.support import requires
+from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
+import tkinter.messagebox as tkMessageBox
+from idlelib import SearchEngine as se
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib.idle_test.mock_tk import Text as mockText
+
+def setUpModule():
+ # Replace s-e module tkinter imports other than non-gui TclError.
+ se.BooleanVar = Var
+ se.StringVar = Var
+ se.tkMessageBox = Mbox
+
+def tearDownModule():
+ # Restore 'just in case', though other tests should also replace.
+ se.BooleanVar = BooleanVar
+ se.StringVar = StringVar
+ se.tkMessageBox = tkMessageBox
+
+
+class Mock:
+ def __init__(self, *args, **kwargs): pass
+
+class GetTest(unittest.TestCase):
+ # SearchEngine.get returns singleton created & saved on first call.
+ def test_get(self):
+ saved_Engine = se.SearchEngine
+ se.SearchEngine = Mock # monkey-patch class
+ try:
+ root = Mock()
+ engine = se.get(root)
+ self.assertIsInstance(engine, se.SearchEngine)
+ self.assertIs(root._searchengine, engine)
+ self.assertIs(se.get(root), engine)
+ finally:
+ se.SearchEngine = saved_Engine # restore class to module
+
+class GetLineColTest(unittest.TestCase):
+ # Test simple text-independent helper function
+ def test_get_line_col(self):
+ self.assertEqual(se.get_line_col('1.0'), (1, 0))
+ self.assertEqual(se.get_line_col('1.11'), (1, 11))
+
+ self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
+ self.assertRaises(ValueError, se.get_line_col, ('end'))
+
+class GetSelectionTest(unittest.TestCase):
+ # Test text-dependent helper function.
+## # Need gui for text.index('sel.first/sel.last/insert').
+## @classmethod
+## def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+##
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_get_selection(self):
+ # text = Text(master=self.root)
+ text = mockText()
+ text.insert('1.0', 'Hello World!')
+
+ # fix text.index result when called in get_selection
+ def sel(s):
+ # select entire text, cursor irrelevant
+ if s == 'sel.first': return '1.0'
+ if s == 'sel.last': return '1.12'
+ raise TclError
+ text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
+ self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark # replaces .mark_set('insert', '1.5')
+ self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
+
+
+class ReverseSearchTest(unittest.TestCase):
+ # Test helper function that searches backwards within a line.
+ def test_search_reverse(self):
+ Equal = self.assertEqual
+ line = "Here is an 'is' test text."
+ prog = re.compile('is')
+ Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 6), None)
+
+
+class SearchEngineTest(unittest.TestCase):
+ # Test class methods that do not use Text widget.
+
+ def setUp(self):
+ self.engine = se.SearchEngine(root=None)
+ # Engine.root is only used to create error message boxes.
+ # The mock replacement ignores the root argument.
+
+ def test_is_get(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getpat(), 'hello')
+
+ Equal(engine.isre(), False)
+ engine.revar.set(1)
+ Equal(engine.isre(), True)
+
+ Equal(engine.iscase(), False)
+ engine.casevar.set(1)
+ Equal(engine.iscase(), True)
+
+ Equal(engine.isword(), False)
+ engine.wordvar.set(1)
+ Equal(engine.isword(), True)
+
+ Equal(engine.iswrap(), True)
+ engine.wrapvar.set(0)
+ Equal(engine.iswrap(), False)
+
+ Equal(engine.isback(), False)
+ engine.backvar.set(1)
+ Equal(engine.isback(), True)
+
+ def test_setcookedpat(self):
+ engine = self.engine
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), '\s')
+ engine.revar.set(1)
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), r'\\s')
+
+ def test_getcookedpat(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getcookedpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getcookedpat(), 'hello')
+ engine.wordvar.set(True)
+ Equal(engine.getcookedpat(), r'\bhello\b')
+ engine.wordvar.set(False)
+
+ engine.setpat('\s')
+ Equal(engine.getcookedpat(), r'\\s')
+ engine.revar.set(True)
+ Equal(engine.getcookedpat(), '\s')
+
+ def test_getprog(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ engine.setpat('Hello')
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
+ engine.casevar.set(1)
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello').pattern, 0)
+
+ engine.setpat('')
+ Equal(engine.getprog(), None)
+ engine.setpat('+')
+ engine.revar.set(1)
+ Equal(engine.getprog(), None)
+ self.assertEqual(Mbox.showerror.message,
+ 'Error: nothing to repeat\nPattern: +')
+
+ def test_report_error(self):
+ showerror = Mbox.showerror
+ Equal = self.assertEqual
+ pat = '[a-z'
+ msg = 'unexpected end of regular expression'
+
+ Equal(self.engine.report_error(pat, msg), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message = ("Error: " + msg + "\nPattern: [a-z")
+ Equal(showerror.message, expected_message)
+
+ Equal(self.engine.report_error(pat, msg, 5), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message += "\nOffset: 5"
+ Equal(showerror.message, expected_message)
+
+
+class SearchTest(unittest.TestCase):
+ # Test that search_text makes right call to right method.
+
+ @classmethod
+ def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+
+ cls.engine = se.SearchEngine(None)
+ cls.engine.search_forward = lambda *args: ('f', args)
+ cls.engine.search_backward = lambda *args: ('b', args)
+
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_search(self):
+ Equal = self.assertEqual
+ engine = self.engine
+ search = engine.search_text
+ text = self.text
+ pat = self.pat
+
+ engine.patvar.set(None)
+ #engine.revar.set(pat)
+ Equal(search(text), None)
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
+ engine.wrapvar.set(False)
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
+ engine.wrapvar.set(True)
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
+ engine.backvar.set(False)
+
+ def sel(s):
+ if s == 'sel.first': return '2.10'
+ if s == 'sel.last': return '2.16'
+ raise TclError
+ text.index = sel
+ Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
+ Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
+ Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
+
+
+class ForwardBackwardTest(unittest.TestCase):
+ # Test that search_forward method finds the target.
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = se.SearchEngine(None)
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ # search_backward calls index('end-1c')
+ cls.text.index = lambda index: '4.0'
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+ cls.res = (2, (10, 16)) # line, slice indexes of 'target'
+ cls.failpat = re.compile('xyz') # not in text
+ cls.emptypat = re.compile('\w*') # empty match possible
+
+ def make_search(self, func):
+ def search(pat, line, col, wrap, ok=0):
+ res = func(self.text, pat, line, col, wrap, ok)
+ # res is (line, matchobject) or None
+ return (res[0], res[1].span()) if res else res
+ return search
+
+ def test_search_forward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ forward = self.make_search(self.engine.search_forward)
+ pat = self.pat
+ Equal(forward(pat, 1, 0, True), self.res)
+ Equal(forward(pat, 3, 0, True), self.res) # wrap
+ Equal(forward(pat, 3, 0, False), None) # no wrap
+ Equal(forward(pat, 2, 10, False), self.res)
+
+ Equal(forward(self.failpat, 1, 0, True), None)
+ Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
+ #Equal(forward(self.emptypat, 2, 9, True), self.res)
+ # While the initial empty match is correctly ignored, skipping
+ # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
+ Equal(forward(self.emptypat, 2, 10, True), self.res)
+
+ def test_search_backward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ backward = self.make_search(self.engine.search_backward)
+ pat = self.pat
+ Equal(backward(pat, 3, 5, True), self.res)
+ Equal(backward(pat, 2, 0, True), self.res) # wrap
+ Equal(backward(pat, 2, 0, False), None) # no wrap
+ Equal(backward(pat, 2, 16, False), self.res)
+
+ Equal(backward(self.failpat, 3, 9, True), None)
+ Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
+ # Accepted because 9 < 10, not because ok=True.
+ # It is not clear that ok=True is useful going back - tjr
+ Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -177,6 +177,11 @@
possible, since "localhost" goes through a DNS lookup under recent Windows
versions.
+IDLE
+----
+
+- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster.
+
Documentation
-------------
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/dfbf0f9034cc
changeset: 85477:dfbf0f9034cc
branch: 3.3
parent: 85466:c11754defe1c
user: Terry Jan Reedy <tjreedy(a)udel.edu>
date: Sat Aug 31 16:27:16 2013 -0400
summary:
Issue #18489: Add complete, gui-free tests for idlelib.SearchEngine.
Patch import and initialization in SearchEngine to make testing easier.
Improve docstrings, especially to clarify the double role of 'ok' parameters.
Original patch by Phil Webster.
files:
Lib/idlelib/SearchEngine.py | 72 +-
Lib/idlelib/idle_test/test_searchengine.py | 326 ++++++++++
Misc/NEWS | 2 +
3 files changed, 366 insertions(+), 34 deletions(-)
diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py
--- a/Lib/idlelib/SearchEngine.py
+++ b/Lib/idlelib/SearchEngine.py
@@ -1,6 +1,6 @@
'''Define SearchEngine for search dialogs.'''
import re
-from tkinter import *
+from tkinter import StringVar, BooleanVar, TclError
import tkinter.messagebox as tkMessageBox
def get(root):
@@ -22,14 +22,13 @@
The dialogs bind these to the UI elements present in the dialogs.
'''
- self.root = root
- self.patvar = StringVar(root) # search pattern
- self.revar = BooleanVar(root) # regular expression?
- self.casevar = BooleanVar(root) # match case?
- self.wordvar = BooleanVar(root) # match whole word?
- self.wrapvar = BooleanVar(root) # wrap around buffer?
- self.wrapvar.set(1) # (on by default)
- self.backvar = BooleanVar(root) # search backwards?
+ self.root = root # need for report_error()
+ self.patvar = StringVar(root, '') # search pattern
+ self.revar = BooleanVar(root, False) # regular expression?
+ self.casevar = BooleanVar(root, False) # match case?
+ self.wordvar = BooleanVar(root, False) # match whole word?
+ self.wrapvar = BooleanVar(root, True) # wrap around buffer?
+ self.backvar = BooleanVar(root, False) # search backwards?
# Access methods
@@ -56,9 +55,16 @@
# Higher level access methods
+ def setcookedpat(self, pat):
+ "Set pattern after escaping if re."
+ # called only in SearchDialog.py: 66
+ if self.isre():
+ pat = re.escape(pat)
+ self.setpat(pat)
+
def getcookedpat(self):
pat = self.getpat()
- if not self.isre():
+ if not self.isre(): # if True, see setcookedpat
pat = re.escape(pat)
if self.isword():
pat = r"\b%s\b" % pat
@@ -90,33 +96,28 @@
# Derived class could override this with something fancier
msg = "Error: " + str(msg)
if pat:
- msg = msg + "\np\Pattern: " + str(pat)
+ msg = msg + "\nPattern: " + str(pat)
if col >= 0:
msg = msg + "\nOffset: " + str(col)
tkMessageBox.showerror("Regular expression error",
msg, master=self.root)
- def setcookedpat(self, pat):
- if self.isre():
- pat = re.escape(pat)
- self.setpat(pat)
+ def search_text(self, text, prog=None, ok=0):
+ '''Return (lineno, matchobj) or None for forward/backward search.
- def search_text(self, text, prog=None, ok=0):
- '''Return (lineno, matchobj) for prog in text widget, or None.
+ This function calls the right function with the right arguments.
+ It directly return the result of that call.
- If prog is given, it should be a precompiled pattern.
- Wrap (yes/no) and direction (forward/back) settings are used.
+ Text is a text widget. Prog is a precompiled pattern.
+ The ok parameteris a bit complicated as it has two effects.
- The search starts at the selection (if there is one) or at the
- insert mark (otherwise). If the search is forward, it starts
- at the right of the selection; for a backward search, it
- starts at the left end. An empty match exactly at either end
- of the selection (or at the insert mark if there is no
- selection) is ignored unless the ok flag is true -- this is
- done to guarantee progress.
+ If there is a selection, the search begin at either end,
+ depending on the direction setting and ok, with ok meaning that
+ the search starts with the selection. Otherwise, search begins
+ at the insert mark.
- If the search is allowed to wrap around, it will return the
- original selection if (and only if) it is the only match.
+ To aid progress, the search functions do not return an empty
+ match at the starting position unless ok is True.
'''
if not prog:
@@ -188,15 +189,18 @@
return None
def search_reverse(prog, chars, col):
- '''Search backwards in a string (line of text).
+ '''Search backwards and return an re match object or None.
This is done by searching forwards until there is no match.
+ Prog: compiled re object with a search method returning a match.
+ Chars: line of text, without \n.
+ Col: stop index for the search; the limit for match.end().
'''
m = prog.search(chars)
if not m:
return None
found = None
- i, j = m.span()
+ i, j = m.span() # m.start(), m.end() == match slice indexes
while i < col and j <= col:
found = m
if i == j:
@@ -226,7 +230,7 @@
line, col = map(int, index.split(".")) # Fails on invalid index
return line, col
-##if __name__ == "__main__":
-## from test import support; support.use_resources = ['gui']
-## import unittest
-## unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
+if __name__ == "__main__":
+ from test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
new file mode 100644
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_searchengine.py
@@ -0,0 +1,326 @@
+'''Test functions and SearchEngine class in SearchEngine.py.'''
+
+# With mock replacements, the module does not use any gui widgets.
+# The use of tk.Text is avoided (for now, until mock Text is improved)
+# by patching instances with an index function returning what is needed.
+# This works because mock Text.get does not use .index.
+
+import re
+import unittest
+from test.support import requires
+from tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
+import tkinter.messagebox as tkMessageBox
+from idlelib import SearchEngine as se
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib.idle_test.mock_tk import Text as mockText
+
+def setUpModule():
+ # Replace s-e module tkinter imports other than non-gui TclError.
+ se.BooleanVar = Var
+ se.StringVar = Var
+ se.tkMessageBox = Mbox
+
+def tearDownModule():
+ # Restore 'just in case', though other tests should also replace.
+ se.BooleanVar = BooleanVar
+ se.StringVar = StringVar
+ se.tkMessageBox = tkMessageBox
+
+
+class Mock:
+ def __init__(self, *args, **kwargs): pass
+
+class GetTest(unittest.TestCase):
+ # SearchEngine.get returns singleton created & saved on first call.
+ def test_get(self):
+ saved_Engine = se.SearchEngine
+ se.SearchEngine = Mock # monkey-patch class
+ try:
+ root = Mock()
+ engine = se.get(root)
+ self.assertIsInstance(engine, se.SearchEngine)
+ self.assertIs(root._searchengine, engine)
+ self.assertIs(se.get(root), engine)
+ finally:
+ se.SearchEngine = saved_Engine # restore class to module
+
+class GetLineColTest(unittest.TestCase):
+ # Test simple text-independent helper function
+ def test_get_line_col(self):
+ self.assertEqual(se.get_line_col('1.0'), (1, 0))
+ self.assertEqual(se.get_line_col('1.11'), (1, 11))
+
+ self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
+ self.assertRaises(ValueError, se.get_line_col, ('end'))
+
+class GetSelectionTest(unittest.TestCase):
+ # Test text-dependent helper function.
+## # Need gui for text.index('sel.first/sel.last/insert').
+## @classmethod
+## def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+##
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_get_selection(self):
+ # text = Text(master=self.root)
+ text = mockText()
+ text.insert('1.0', 'Hello World!')
+
+ # fix text.index result when called in get_selection
+ def sel(s):
+ # select entire text, cursor irrelevant
+ if s == 'sel.first': return '1.0'
+ if s == 'sel.last': return '1.12'
+ raise TclError
+ text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
+ self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark # replaces .mark_set('insert', '1.5')
+ self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
+
+
+class ReverseSearchTest(unittest.TestCase):
+ # Test helper function that searches backwards within a line.
+ def test_search_reverse(self):
+ Equal = self.assertEqual
+ line = "Here is an 'is' test text."
+ prog = re.compile('is')
+ Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 6), None)
+
+
+class SearchEngineTest(unittest.TestCase):
+ # Test class methods that do not use Text widget.
+
+ def setUp(self):
+ self.engine = se.SearchEngine(root=None)
+ # Engine.root is only used to create error message boxes.
+ # The mock replacement ignores the root argument.
+
+ def test_is_get(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getpat(), 'hello')
+
+ Equal(engine.isre(), False)
+ engine.revar.set(1)
+ Equal(engine.isre(), True)
+
+ Equal(engine.iscase(), False)
+ engine.casevar.set(1)
+ Equal(engine.iscase(), True)
+
+ Equal(engine.isword(), False)
+ engine.wordvar.set(1)
+ Equal(engine.isword(), True)
+
+ Equal(engine.iswrap(), True)
+ engine.wrapvar.set(0)
+ Equal(engine.iswrap(), False)
+
+ Equal(engine.isback(), False)
+ engine.backvar.set(1)
+ Equal(engine.isback(), True)
+
+ def test_setcookedpat(self):
+ engine = self.engine
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), '\s')
+ engine.revar.set(1)
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), r'\\s')
+
+ def test_getcookedpat(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getcookedpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getcookedpat(), 'hello')
+ engine.wordvar.set(True)
+ Equal(engine.getcookedpat(), r'\bhello\b')
+ engine.wordvar.set(False)
+
+ engine.setpat('\s')
+ Equal(engine.getcookedpat(), r'\\s')
+ engine.revar.set(True)
+ Equal(engine.getcookedpat(), '\s')
+
+ def test_getprog(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ engine.setpat('Hello')
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
+ engine.casevar.set(1)
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello').pattern, 0)
+
+ engine.setpat('')
+ Equal(engine.getprog(), None)
+ engine.setpat('+')
+ engine.revar.set(1)
+ Equal(engine.getprog(), None)
+ self.assertEqual(Mbox.showerror.message,
+ 'Error: nothing to repeat\nPattern: +')
+
+ def test_report_error(self):
+ showerror = Mbox.showerror
+ Equal = self.assertEqual
+ pat = '[a-z'
+ msg = 'unexpected end of regular expression'
+
+ Equal(self.engine.report_error(pat, msg), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message = ("Error: " + msg + "\nPattern: [a-z")
+ Equal(showerror.message, expected_message)
+
+ Equal(self.engine.report_error(pat, msg, 5), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message += "\nOffset: 5"
+ Equal(showerror.message, expected_message)
+
+
+class SearchTest(unittest.TestCase):
+ # Test that search_text makes right call to right method.
+
+ @classmethod
+ def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+
+ cls.engine = se.SearchEngine(None)
+ cls.engine.search_forward = lambda *args: ('f', args)
+ cls.engine.search_backward = lambda *args: ('b', args)
+
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_search(self):
+ Equal = self.assertEqual
+ engine = self.engine
+ search = engine.search_text
+ text = self.text
+ pat = self.pat
+
+ engine.patvar.set(None)
+ #engine.revar.set(pat)
+ Equal(search(text), None)
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
+ engine.wrapvar.set(False)
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
+ engine.wrapvar.set(True)
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
+ engine.backvar.set(False)
+
+ def sel(s):
+ if s == 'sel.first': return '2.10'
+ if s == 'sel.last': return '2.16'
+ raise TclError
+ text.index = sel
+ Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
+ Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
+ Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
+
+
+class ForwardBackwardTest(unittest.TestCase):
+ # Test that search_forward method finds the target.
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = se.SearchEngine(None)
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ # search_backward calls index('end-1c')
+ cls.text.index = lambda index: '4.0'
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+ cls.res = (2, (10, 16)) # line, slice indexes of 'target'
+ cls.failpat = re.compile('xyz') # not in text
+ cls.emptypat = re.compile('\w*') # empty match possible
+
+ def make_search(self, func):
+ def search(pat, line, col, wrap, ok=0):
+ res = func(self.text, pat, line, col, wrap, ok)
+ # res is (line, matchobject) or None
+ return (res[0], res[1].span()) if res else res
+ return search
+
+ def test_search_forward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ forward = self.make_search(self.engine.search_forward)
+ pat = self.pat
+ Equal(forward(pat, 1, 0, True), self.res)
+ Equal(forward(pat, 3, 0, True), self.res) # wrap
+ Equal(forward(pat, 3, 0, False), None) # no wrap
+ Equal(forward(pat, 2, 10, False), self.res)
+
+ Equal(forward(self.failpat, 1, 0, True), None)
+ Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
+ #Equal(forward(self.emptypat, 2, 9, True), self.res)
+ # While the initial empty match is correctly ignored, skipping
+ # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
+ Equal(forward(self.emptypat, 2, 10, True), self.res)
+
+ def test_search_backward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ backward = self.make_search(self.engine.search_backward)
+ pat = self.pat
+ Equal(backward(pat, 3, 5, True), self.res)
+ Equal(backward(pat, 2, 0, True), self.res) # wrap
+ Equal(backward(pat, 2, 0, False), None) # no wrap
+ Equal(backward(pat, 2, 16, False), self.res)
+
+ Equal(backward(self.failpat, 3, 9, True), None)
+ Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
+ # Accepted because 9 < 10, not because ok=True.
+ # It is not clear that ok=True is useful going back - tjr
+ Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -275,6 +275,8 @@
IDLE
----
+- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster.
+
- Issue #18429: Format / Format Paragraph, now works when comment blocks
are selected. As with text blocks, this works best when the selection
only includes complete lines.
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/4179e2312089
changeset: 85476:4179e2312089
branch: 2.7
parent: 85465:43749cb6bdbd
user: Terry Jan Reedy <tjreedy(a)udel.edu>
date: Sat Aug 31 16:27:08 2013 -0400
summary:
Issue #18489: Add complete, gui-free tests for idlelib.SearchEngine.
Patch import and initialization in SearchEngine to make testing easier.
Improve docstrings, especially to clarify the double role of 'ok' parameters.
Original patch by Phil Webster.
files:
Lib/idlelib/SearchEngine.py | 72 +-
Lib/idlelib/idle_test/test_searchengine.py | 326 ++++++++++
Misc/NEWS | 2 +
3 files changed, 366 insertions(+), 34 deletions(-)
diff --git a/Lib/idlelib/SearchEngine.py b/Lib/idlelib/SearchEngine.py
--- a/Lib/idlelib/SearchEngine.py
+++ b/Lib/idlelib/SearchEngine.py
@@ -1,6 +1,6 @@
'''Define SearchEngine for search dialogs.'''
import re
-from Tkinter import *
+from Tkinter import StringVar, BooleanVar, TclError
import tkMessageBox
def get(root):
@@ -22,14 +22,13 @@
The dialogs bind these to the UI elements present in the dialogs.
'''
- self.root = root
- self.patvar = StringVar(root) # search pattern
- self.revar = BooleanVar(root) # regular expression?
- self.casevar = BooleanVar(root) # match case?
- self.wordvar = BooleanVar(root) # match whole word?
- self.wrapvar = BooleanVar(root) # wrap around buffer?
- self.wrapvar.set(1) # (on by default)
- self.backvar = BooleanVar(root) # search backwards?
+ self.root = root # need for report_error()
+ self.patvar = StringVar(root, '') # search pattern
+ self.revar = BooleanVar(root, False) # regular expression?
+ self.casevar = BooleanVar(root, False) # match case?
+ self.wordvar = BooleanVar(root, False) # match whole word?
+ self.wrapvar = BooleanVar(root, True) # wrap around buffer?
+ self.backvar = BooleanVar(root, False) # search backwards?
# Access methods
@@ -56,9 +55,16 @@
# Higher level access methods
+ def setcookedpat(self, pat):
+ "Set pattern after escaping if re."
+ # called only in SearchDialog.py: 66
+ if self.isre():
+ pat = re.escape(pat)
+ self.setpat(pat)
+
def getcookedpat(self):
pat = self.getpat()
- if not self.isre():
+ if not self.isre(): # if True, see setcookedpat
pat = re.escape(pat)
if self.isword():
pat = r"\b%s\b" % pat
@@ -90,33 +96,28 @@
# Derived class could override this with something fancier
msg = "Error: " + str(msg)
if pat:
- msg = msg + "\np\Pattern: " + str(pat)
+ msg = msg + "\nPattern: " + str(pat)
if col >= 0:
msg = msg + "\nOffset: " + str(col)
tkMessageBox.showerror("Regular expression error",
msg, master=self.root)
- def setcookedpat(self, pat):
- if self.isre():
- pat = re.escape(pat)
- self.setpat(pat)
+ def search_text(self, text, prog=None, ok=0):
+ '''Return (lineno, matchobj) or None for forward/backward search.
- def search_text(self, text, prog=None, ok=0):
- '''Return (lineno, matchobj) for prog in text widget, or None.
+ This function calls the right function with the right arguments.
+ It directly return the result of that call.
- If prog is given, it should be a precompiled pattern.
- Wrap (yes/no) and direction (forward/back) settings are used.
+ Text is a text widget. Prog is a precompiled pattern.
+ The ok parameteris a bit complicated as it has two effects.
- The search starts at the selection (if there is one) or at the
- insert mark (otherwise). If the search is forward, it starts
- at the right of the selection; for a backward search, it
- starts at the left end. An empty match exactly at either end
- of the selection (or at the insert mark if there is no
- selection) is ignored unless the ok flag is true -- this is
- done to guarantee progress.
+ If there is a selection, the search begin at either end,
+ depending on the direction setting and ok, with ok meaning that
+ the search starts with the selection. Otherwise, search begins
+ at the insert mark.
- If the search is allowed to wrap around, it will return the
- original selection if (and only if) it is the only match.
+ To aid progress, the search functions do not return an empty
+ match at the starting position unless ok is True.
'''
if not prog:
@@ -188,15 +189,18 @@
return None
def search_reverse(prog, chars, col):
- '''Search backwards in a string (line of text).
+ '''Search backwards and return an re match object or None.
This is done by searching forwards until there is no match.
+ Prog: compiled re object with a search method returning a match.
+ Chars: line of text, without \n.
+ Col: stop index for the search; the limit for match.end().
'''
m = prog.search(chars)
if not m:
return None
found = None
- i, j = m.span()
+ i, j = m.span() # m.start(), m.end() == match slice indexes
while i < col and j <= col:
found = m
if i == j:
@@ -226,7 +230,7 @@
line, col = map(int, index.split(".")) # Fails on invalid index
return line, col
-##if __name__ == "__main__":
-## from test import support; support.use_resources = ['gui']
-## import unittest
-## unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
+if __name__ == "__main__":
+ from test import support; support.use_resources = ['gui']
+ import unittest
+ unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
diff --git a/Lib/idlelib/idle_test/test_searchengine.py b/Lib/idlelib/idle_test/test_searchengine.py
new file mode 100644
--- /dev/null
+++ b/Lib/idlelib/idle_test/test_searchengine.py
@@ -0,0 +1,326 @@
+'''Test functions and SearchEngine class in SearchEngine.py.'''
+
+# With mock replacements, the module does not use any gui widgets.
+# The use of tk.Text is avoided (for now, until mock Text is improved)
+# by patching instances with an index function returning what is needed.
+# This works because mock Text.get does not use .index.
+
+import re
+import unittest
+from test.test_support import requires
+from Tkinter import BooleanVar, StringVar, TclError # ,Tk, Text
+import tkMessageBox
+from idlelib import SearchEngine as se
+from idlelib.idle_test.mock_tk import Var, Mbox
+from idlelib.idle_test.mock_tk import Text as mockText
+
+def setUpModule():
+ # Replace s-e module tkinter imports other than non-gui TclError.
+ se.BooleanVar = Var
+ se.StringVar = Var
+ se.tkMessageBox = Mbox
+
+def tearDownModule():
+ # Restore 'just in case', though other tests should also replace.
+ se.BooleanVar = BooleanVar
+ se.StringVar = StringVar
+ se.tkMessageBox = tkMessageBox
+
+
+class Mock:
+ def __init__(self, *args, **kwargs): pass
+
+class GetTest(unittest.TestCase):
+ # SearchEngine.get returns singleton created & saved on first call.
+ def test_get(self):
+ saved_Engine = se.SearchEngine
+ se.SearchEngine = Mock # monkey-patch class
+ try:
+ root = Mock()
+ engine = se.get(root)
+ self.assertIsInstance(engine, se.SearchEngine)
+ self.assertIs(root._searchengine, engine)
+ self.assertIs(se.get(root), engine)
+ finally:
+ se.SearchEngine = saved_Engine # restore class to module
+
+class GetLineColTest(unittest.TestCase):
+ # Test simple text-independent helper function
+ def test_get_line_col(self):
+ self.assertEqual(se.get_line_col('1.0'), (1, 0))
+ self.assertEqual(se.get_line_col('1.11'), (1, 11))
+
+ self.assertRaises(ValueError, se.get_line_col, ('1.0 lineend'))
+ self.assertRaises(ValueError, se.get_line_col, ('end'))
+
+class GetSelectionTest(unittest.TestCase):
+ # Test text-dependent helper function.
+## # Need gui for text.index('sel.first/sel.last/insert').
+## @classmethod
+## def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+##
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_get_selection(self):
+ # text = Text(master=self.root)
+ text = mockText()
+ text.insert('1.0', 'Hello World!')
+
+ # fix text.index result when called in get_selection
+ def sel(s):
+ # select entire text, cursor irrelevant
+ if s == 'sel.first': return '1.0'
+ if s == 'sel.last': return '1.12'
+ raise TclError
+ text.index = sel # replaces .tag_add('sel', '1.0, '1.12')
+ self.assertEqual(se.get_selection(text), ('1.0', '1.12'))
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark # replaces .mark_set('insert', '1.5')
+ self.assertEqual(se.get_selection(text), ('1.5', '1.5'))
+
+
+class ReverseSearchTest(unittest.TestCase):
+ # Test helper function that searches backwards within a line.
+ def test_search_reverse(self):
+ Equal = self.assertEqual
+ line = "Here is an 'is' test text."
+ prog = re.compile('is')
+ Equal(se.search_reverse(prog, line, len(line)).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 14).span(), (12, 14))
+ Equal(se.search_reverse(prog, line, 13).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 7).span(), (5, 7))
+ Equal(se.search_reverse(prog, line, 6), None)
+
+
+class SearchEngineTest(unittest.TestCase):
+ # Test class methods that do not use Text widget.
+
+ def setUp(self):
+ self.engine = se.SearchEngine(root=None)
+ # Engine.root is only used to create error message boxes.
+ # The mock replacement ignores the root argument.
+
+ def test_is_get(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getpat(), 'hello')
+
+ Equal(engine.isre(), False)
+ engine.revar.set(1)
+ Equal(engine.isre(), True)
+
+ Equal(engine.iscase(), False)
+ engine.casevar.set(1)
+ Equal(engine.iscase(), True)
+
+ Equal(engine.isword(), False)
+ engine.wordvar.set(1)
+ Equal(engine.isword(), True)
+
+ Equal(engine.iswrap(), True)
+ engine.wrapvar.set(0)
+ Equal(engine.iswrap(), False)
+
+ Equal(engine.isback(), False)
+ engine.backvar.set(1)
+ Equal(engine.isback(), True)
+
+ def test_setcookedpat(self):
+ engine = self.engine
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), '\s')
+ engine.revar.set(1)
+ engine.setcookedpat('\s')
+ self.assertEqual(engine.getpat(), r'\\s')
+
+ def test_getcookedpat(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ Equal(engine.getcookedpat(), '')
+ engine.setpat('hello')
+ Equal(engine.getcookedpat(), 'hello')
+ engine.wordvar.set(True)
+ Equal(engine.getcookedpat(), r'\bhello\b')
+ engine.wordvar.set(False)
+
+ engine.setpat('\s')
+ Equal(engine.getcookedpat(), r'\\s')
+ engine.revar.set(True)
+ Equal(engine.getcookedpat(), '\s')
+
+ def test_getprog(self):
+ engine = self.engine
+ Equal = self.assertEqual
+
+ engine.setpat('Hello')
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello', re.IGNORECASE).pattern)
+ engine.casevar.set(1)
+ temppat = engine.getprog()
+ Equal(temppat.pattern, re.compile('Hello').pattern, 0)
+
+ engine.setpat('')
+ Equal(engine.getprog(), None)
+ engine.setpat('+')
+ engine.revar.set(1)
+ Equal(engine.getprog(), None)
+ self.assertEqual(Mbox.showerror.message,
+ 'Error: nothing to repeat\nPattern: +')
+
+ def test_report_error(self):
+ showerror = Mbox.showerror
+ Equal = self.assertEqual
+ pat = '[a-z'
+ msg = 'unexpected end of regular expression'
+
+ Equal(self.engine.report_error(pat, msg), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message = ("Error: " + msg + "\nPattern: [a-z")
+ Equal(showerror.message, expected_message)
+
+ Equal(self.engine.report_error(pat, msg, 5), None)
+ Equal(showerror.title, 'Regular expression error')
+ expected_message += "\nOffset: 5"
+ Equal(showerror.message, expected_message)
+
+
+class SearchTest(unittest.TestCase):
+ # Test that search_text makes right call to right method.
+
+ @classmethod
+ def setUpClass(cls):
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+
+ cls.engine = se.SearchEngine(None)
+ cls.engine.search_forward = lambda *args: ('f', args)
+ cls.engine.search_backward = lambda *args: ('b', args)
+
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ def test_search(self):
+ Equal = self.assertEqual
+ engine = self.engine
+ search = engine.search_text
+ text = self.text
+ pat = self.pat
+
+ engine.patvar.set(None)
+ #engine.revar.set(pat)
+ Equal(search(text), None)
+
+ def mark(s):
+ # no selection, cursor after 'Hello'
+ if s == 'insert': return '1.5'
+ raise TclError
+ text.index = mark
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, True, False)))
+ engine.wrapvar.set(False)
+ Equal(search(text, pat), ('f', (text, pat, 1, 5, False, False)))
+ engine.wrapvar.set(True)
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 1, 5, True, False)))
+ engine.backvar.set(False)
+
+ def sel(s):
+ if s == 'sel.first': return '2.10'
+ if s == 'sel.last': return '2.16'
+ raise TclError
+ text.index = sel
+ Equal(search(text, pat), ('f', (text, pat, 2, 16, True, False)))
+ Equal(search(text, pat, True), ('f', (text, pat, 2, 10, True, True)))
+ engine.backvar.set(True)
+ Equal(search(text, pat), ('b', (text, pat, 2, 10, True, False)))
+ Equal(search(text, pat, True), ('b', (text, pat, 2, 16, True, True)))
+
+
+class ForwardBackwardTest(unittest.TestCase):
+ # Test that search_forward method finds the target.
+## @classmethod
+## def tearDownClass(cls):
+## cls.root.destroy()
+
+ @classmethod
+ def setUpClass(cls):
+ cls.engine = se.SearchEngine(None)
+## requires('gui')
+## cls.root = Tk()
+## cls.text = Text(master=cls.root)
+ cls.text = mockText()
+ # search_backward calls index('end-1c')
+ cls.text.index = lambda index: '4.0'
+ test_text = (
+ 'First line\n'
+ 'Line with target\n'
+ 'Last line\n')
+ cls.text.insert('1.0', test_text)
+ cls.pat = re.compile('target')
+ cls.res = (2, (10, 16)) # line, slice indexes of 'target'
+ cls.failpat = re.compile('xyz') # not in text
+ cls.emptypat = re.compile('\w*') # empty match possible
+
+ def make_search(self, func):
+ def search(pat, line, col, wrap, ok=0):
+ res = func(self.text, pat, line, col, wrap, ok)
+ # res is (line, matchobject) or None
+ return (res[0], res[1].span()) if res else res
+ return search
+
+ def test_search_forward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ forward = self.make_search(self.engine.search_forward)
+ pat = self.pat
+ Equal(forward(pat, 1, 0, True), self.res)
+ Equal(forward(pat, 3, 0, True), self.res) # wrap
+ Equal(forward(pat, 3, 0, False), None) # no wrap
+ Equal(forward(pat, 2, 10, False), self.res)
+
+ Equal(forward(self.failpat, 1, 0, True), None)
+ Equal(forward(self.emptypat, 2, 9, True, ok=True), (2, (9, 9)))
+ #Equal(forward(self.emptypat, 2, 9, True), self.res)
+ # While the initial empty match is correctly ignored, skipping
+ # the rest of the line and returning (3, (0,4)) seems buggy - tjr.
+ Equal(forward(self.emptypat, 2, 10, True), self.res)
+
+ def test_search_backward(self):
+ # search for non-empty match
+ Equal = self.assertEqual
+ backward = self.make_search(self.engine.search_backward)
+ pat = self.pat
+ Equal(backward(pat, 3, 5, True), self.res)
+ Equal(backward(pat, 2, 0, True), self.res) # wrap
+ Equal(backward(pat, 2, 0, False), None) # no wrap
+ Equal(backward(pat, 2, 16, False), self.res)
+
+ Equal(backward(self.failpat, 3, 9, True), None)
+ Equal(backward(self.emptypat, 2, 10, True, ok=True), (2, (9,9)))
+ # Accepted because 9 < 10, not because ok=True.
+ # It is not clear that ok=True is useful going back - tjr
+ Equal(backward(self.emptypat, 2, 9, True), (2, (5, 9)))
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2, exit=2)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -171,6 +171,8 @@
IDLE
----
+- Issue #18489: Add tests for SearchEngine. Original patch by Phil Webster.
+
- Issue #18429: Format / Format Paragraph, now works when comment blocks
are selected. As with text blocks, this works best when the selection
only includes complete lines.
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/833246d42825
changeset: 85475:833246d42825
user: Ethan Furman <ethan(a)stoneleaf.us>
date: Sat Aug 31 12:48:51 2013 -0700
summary:
Issue #18780: code cleanup.
files:
Lib/test/test_unicode.py | 53 ++++++++++-----------------
1 files changed, 19 insertions(+), 34 deletions(-)
diff --git a/Lib/test/test_unicode.py b/Lib/test/test_unicode.py
--- a/Lib/test/test_unicode.py
+++ b/Lib/test/test_unicode.py
@@ -1134,42 +1134,27 @@
class Str(str, enum.Enum):
ABC = 'abc'
# Testing Unicode formatting strings...
- self.assertEqual(
- "%s, %s" % (Str.ABC, Str.ABC),
- 'Str.ABC, Str.ABC',
- )
- self.assertEqual(
- "%s, %s, %d, %i, %u, %f, %5.2f" %
- (Str.ABC, Str.ABC,
- Int.IDES, Int.IDES, Int.IDES,
- Float.PI, Float.PI),
- 'Str.ABC, Str.ABC, 15, 15, 15, 3.141593, 3.14')
+ self.assertEqual("%s, %s" % (Str.ABC, Str.ABC),
+ 'Str.ABC, Str.ABC')
+ self.assertEqual("%s, %s, %d, %i, %u, %f, %5.2f" %
+ (Str.ABC, Str.ABC,
+ Int.IDES, Int.IDES, Int.IDES,
+ Float.PI, Float.PI),
+ 'Str.ABC, Str.ABC, 15, 15, 15, 3.141593, 3.14')
# formatting jobs delegated from the string implementation:
- self.assertEqual(
- '...%(foo)s...' % {'foo':Str.ABC},
- '...Str.ABC...',
- )
- self.assertEqual(
- '...%(foo)s...' % {'foo':Int.IDES},
- '...Int.IDES...',
- )
- self.assertEqual(
- '...%(foo)i...' % {'foo':Int.IDES},
- '...15...',
- )
- self.assertEqual(
- '...%(foo)d...' % {'foo':Int.IDES},
- '...15...',
- )
- self.assertEqual(
- '...%(foo)u...' % {'foo':Int.IDES, 'def':Float.PI},
- '...15...',
- )
- self.assertEqual(
- '...%(foo)f...' % {'foo':Float.PI,'def':123},
- '...3.141593...',
- )
+ self.assertEqual('...%(foo)s...' % {'foo':Str.ABC},
+ '...Str.ABC...')
+ self.assertEqual('...%(foo)s...' % {'foo':Int.IDES},
+ '...Int.IDES...')
+ self.assertEqual('...%(foo)i...' % {'foo':Int.IDES},
+ '...15...')
+ self.assertEqual('...%(foo)d...' % {'foo':Int.IDES},
+ '...15...')
+ self.assertEqual('...%(foo)u...' % {'foo':Int.IDES, 'def':Float.PI},
+ '...15...')
+ self.assertEqual('...%(foo)f...' % {'foo':Float.PI,'def':123},
+ '...3.141593...')
@support.cpython_only
def test_formatting_huge_precision(self):
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/868ad6fa8e68
changeset: 85474:868ad6fa8e68
user: Andrew Svetlov <andrew.svetlov(a)gmail.com>
date: Sat Aug 31 20:55:25 2013 +0300
summary:
Temporary disable tests cleanup (issue 11798).
files:
Lib/unittest/suite.py | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
diff --git a/Lib/unittest/suite.py b/Lib/unittest/suite.py
--- a/Lib/unittest/suite.py
+++ b/Lib/unittest/suite.py
@@ -66,6 +66,7 @@
def _removeTestAtIndex(self, index):
"""Stop holding a reference to the TestCase at index."""
+ return
try:
self._tests[index] = None
except TypeError:
--
Repository URL: http://hg.python.org/cpython
http://hg.python.org/cpython/rev/33727fbb4668
changeset: 85473:33727fbb4668
user: Ethan Furman <ethan(a)stoneleaf.us>
date: Sat Aug 31 10:18:55 2013 -0700
summary:
Close #18780: %-formatting now prints value for int subclasses with %d, %i, and %u codes.
files:
Lib/test/test_unicode.py | 47 ++++++++++++++++++++++++++++
Misc/NEWS | 3 +
Objects/unicodeobject.c | 8 +--
3 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/Lib/test/test_unicode.py b/Lib/test/test_unicode.py
--- a/Lib/test/test_unicode.py
+++ b/Lib/test/test_unicode.py
@@ -1124,6 +1124,53 @@
self.assertEqual('%.1s' % "a\xe9\u20ac", 'a')
self.assertEqual('%.2s' % "a\xe9\u20ac", 'a\xe9')
+ def test_formatting_with_enum(self):
+ # issue18780
+ import enum
+ class Float(float, enum.Enum):
+ PI = 3.1415926
+ class Int(enum.IntEnum):
+ IDES = 15
+ class Str(str, enum.Enum):
+ ABC = 'abc'
+ # Testing Unicode formatting strings...
+ self.assertEqual(
+ "%s, %s" % (Str.ABC, Str.ABC),
+ 'Str.ABC, Str.ABC',
+ )
+ self.assertEqual(
+ "%s, %s, %d, %i, %u, %f, %5.2f" %
+ (Str.ABC, Str.ABC,
+ Int.IDES, Int.IDES, Int.IDES,
+ Float.PI, Float.PI),
+ 'Str.ABC, Str.ABC, 15, 15, 15, 3.141593, 3.14')
+
+ # formatting jobs delegated from the string implementation:
+ self.assertEqual(
+ '...%(foo)s...' % {'foo':Str.ABC},
+ '...Str.ABC...',
+ )
+ self.assertEqual(
+ '...%(foo)s...' % {'foo':Int.IDES},
+ '...Int.IDES...',
+ )
+ self.assertEqual(
+ '...%(foo)i...' % {'foo':Int.IDES},
+ '...15...',
+ )
+ self.assertEqual(
+ '...%(foo)d...' % {'foo':Int.IDES},
+ '...15...',
+ )
+ self.assertEqual(
+ '...%(foo)u...' % {'foo':Int.IDES, 'def':Float.PI},
+ '...15...',
+ )
+ self.assertEqual(
+ '...%(foo)f...' % {'foo':Float.PI,'def':123},
+ '...3.141593...',
+ )
+
@support.cpython_only
def test_formatting_huge_precision(self):
from _testcapi import INT_MAX
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -48,6 +48,9 @@
- Issue #17934: Add a clear() method to frame objects, to help clean up
expensive details (local variables) and break reference cycles.
+- Issue #18780: %-formatting codes %d, %i, and %u now treat int-subclasses
+ as int (displays value of int-subclass instead of str(int-subclass) ).
+
Library
-------
diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c
--- a/Objects/unicodeobject.c
+++ b/Objects/unicodeobject.c
@@ -13566,11 +13566,9 @@
case 'd':
case 'i':
case 'u':
- /* Special-case boolean: we want 0/1 */
- if (PyBool_Check(val))
- result = PyNumber_ToBase(val, 10);
- else
- result = Py_TYPE(val)->tp_str(val);
+ /* int and int subclasses should print numerically when a numeric */
+ /* format code is used (see issue18780) */
+ result = PyNumber_ToBase(val, 10);
break;
case 'o':
numnondigits = 2;
--
Repository URL: http://hg.python.org/cpython